Milan Jurkulak 2 роки тому
батько
коміт
e715cc982f
55 змінених файлів з 1773 додано та 480 видалено
  1. 24 0
      .run/mjdev-desktop [cleanUp].run.xml
  2. 24 0
      .run/mjdev-desktop [runDebug].run.xml
  3. 33 76
      build.gradle.kts
  4. 30 0
      buildSrc/build.gradle.kts
  5. 7 0
      buildSrc/settings.gradle.kts
  6. 70 0
      buildSrc/src/main/kotlin/org/mjdev/gradle/extensions/DependencyExt.kt
  7. 91 0
      buildSrc/src/main/kotlin/org/mjdev/gradle/extensions/ProjectExt.kt
  8. 28 0
      buildSrc/src/main/kotlin/org/mjdev/gradle/plugin/DesktopPlugin.kt
  9. 4 1
      gradle/libs.versions.toml
  10. 10 10
      src/main/kotlin/eu/mjdev/desktop/Main.kt
  11. 2 2
      src/main/kotlin/eu/mjdev/desktop/components/controlcenter/ControlCenter.kt
  12. 21 12
      src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/DesktopPanel.kt
  13. 4 5
      src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/DesktopPanelIcon.kt
  14. 0 2
      src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/applets/DesktopPanelFavoriteApps.kt
  15. 1 1
      src/main/kotlin/eu/mjdev/desktop/components/gallery/Gallery.kt
  16. 21 2
      src/main/kotlin/eu/mjdev/desktop/components/image/PhotoImage.kt
  17. 5 1
      src/main/kotlin/eu/mjdev/desktop/components/main/MainWindow.kt
  18. 9 3
      src/main/kotlin/eu/mjdev/desktop/components/sliding/base/VisibilityState.kt
  19. 1 2
      src/main/kotlin/eu/mjdev/desktop/components/text/TextAny.kt
  20. 3 1
      src/main/kotlin/eu/mjdev/desktop/components/tooltip/Tooltip.kt
  21. 50 59
      src/main/kotlin/eu/mjdev/desktop/data/App.kt
  22. 89 34
      src/main/kotlin/eu/mjdev/desktop/data/DesktopFile.kt
  23. 6 0
      src/main/kotlin/eu/mjdev/desktop/extensions/Compose.kt
  24. 7 19
      src/main/kotlin/eu/mjdev/desktop/extensions/Custom.kt
  25. 93 0
      src/main/kotlin/eu/mjdev/desktop/helpers/application/Application.kt
  26. 23 0
      src/main/kotlin/eu/mjdev/desktop/helpers/application/ApplicationApplier.kt
  27. 79 0
      src/main/kotlin/eu/mjdev/desktop/helpers/application/ApplicationScope.kt
  28. 42 0
      src/main/kotlin/eu/mjdev/desktop/helpers/application/base/GlobalSnapshotManager.kt
  29. 41 0
      src/main/kotlin/eu/mjdev/desktop/helpers/application/base/Helpers.kt
  30. 21 0
      src/main/kotlin/eu/mjdev/desktop/helpers/application/base/YieldFrameClock.kt
  31. 27 0
      src/main/kotlin/eu/mjdev/desktop/helpers/application/block/Block.kt
  32. 49 0
      src/main/kotlin/eu/mjdev/desktop/helpers/application/l10n/En.kt
  33. 65 0
      src/main/kotlin/eu/mjdev/desktop/helpers/application/l10n/Strings.kt
  34. 35 0
      src/main/kotlin/eu/mjdev/desktop/helpers/application/l10n/Translations.kt
  35. 13 0
      src/main/kotlin/eu/mjdev/desktop/helpers/exception/ErrorException.kt
  36. 13 0
      src/main/kotlin/eu/mjdev/desktop/helpers/exception/SuccessException.kt
  37. 4 4
      src/main/kotlin/eu/mjdev/desktop/helpers/internal/DpBounds.kt
  38. 2 2
      src/main/kotlin/eu/mjdev/desktop/helpers/internal/Palette.kt
  39. 4 4
      src/main/kotlin/eu/mjdev/desktop/helpers/system/Environment.kt
  40. 37 0
      src/main/kotlin/eu/mjdev/desktop/helpers/system/LinuxIni.kt
  41. 81 99
      src/main/kotlin/eu/mjdev/desktop/helpers/system/Shell.kt
  42. 2 4
      src/main/kotlin/eu/mjdev/desktop/icons/Icons.kt
  43. 1 1
      src/main/kotlin/eu/mjdev/desktop/managers/ConnectivityManager.kt
  44. 1 1
      src/main/kotlin/eu/mjdev/desktop/managers/FileSystemWatcher.kt
  45. 1 1
      src/main/kotlin/eu/mjdev/desktop/managers/GnomeManager.kt
  46. 1 1
      src/main/kotlin/eu/mjdev/desktop/managers/KCEFHelper.kt
  47. 546 0
      src/main/kotlin/eu/mjdev/desktop/managers/WindowsManager.kt
  48. 31 44
      src/main/kotlin/eu/mjdev/desktop/provider/AppsProvider.kt
  49. 13 13
      src/main/kotlin/eu/mjdev/desktop/provider/DesktopProvider.kt
  50. 0 70
      src/main/kotlin/eu/mjdev/desktop/provider/WindowsManager.kt
  51. 2 2
      src/main/kotlin/eu/mjdev/desktop/theme/gtk/GtkThemeHelper.kt
  52. 3 2
      src/main/kotlin/eu/mjdev/desktop/windows/Window.kt
  53. 1 1
      src/main/kotlin/eu/mjdev/desktop/windows/WindowStateHelper.kt
  54. 1 0
      src/main/kotlin/eu/mjdev/desktop/windows/blur/base/HackedContentPane.kt
  55. 1 1
      src/main/kotlin/eu/mjdev/desktop/windows/blur/linux/LinuxWindowBlurManager.kt

+ 24 - 0
.run/mjdev-desktop [cleanUp].run.xml

@@ -0,0 +1,24 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="mjdev-desktop [cleanUp]" type="GradleRunConfiguration" factoryName="Gradle" nameIsGenerated="true">
+    <ExternalSystemSettings>
+      <option name="executionName" />
+      <option name="externalProjectPath" value="$PROJECT_DIR$" />
+      <option name="externalSystemIdString" value="GRADLE" />
+      <option name="scriptParameters" value="" />
+      <option name="taskDescriptions">
+        <list />
+      </option>
+      <option name="taskNames">
+        <list>
+          <option value="cleanUp" />
+        </list>
+      </option>
+      <option name="vmOptions" />
+    </ExternalSystemSettings>
+    <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
+    <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+    <DebugAllEnabled>false</DebugAllEnabled>
+    <RunAsTest>false</RunAsTest>
+    <method v="2" />
+  </configuration>
+</component>

+ 24 - 0
.run/mjdev-desktop [runDebug].run.xml

@@ -0,0 +1,24 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="mjdev-desktop [runDebug]" type="GradleRunConfiguration" factoryName="Gradle">
+    <ExternalSystemSettings>
+      <option name="executionName" />
+      <option name="externalProjectPath" value="$PROJECT_DIR$" />
+      <option name="externalSystemIdString" value="GRADLE" />
+      <option name="scriptParameters" value="" />
+      <option name="taskDescriptions">
+        <list />
+      </option>
+      <option name="taskNames">
+        <list>
+          <option value="runDebug" />
+        </list>
+      </option>
+      <option name="vmOptions" value="--enable-preview" />
+    </ExternalSystemSettings>
+    <ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
+    <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+    <DebugAllEnabled>false</DebugAllEnabled>
+    <RunAsTest>false</RunAsTest>
+    <method v="2" />
+  </configuration>
+</component>

+ 33 - 76
build.gradle.kts

@@ -1,10 +1,13 @@
-import org.jetbrains.compose.desktop.DesktopExtension
 import org.jetbrains.compose.desktop.application.dsl.TargetFormat
+import org.mjdev.gradle.extensions.ProjectExt.createTask
+import org.mjdev.gradle.extensions.ProjectExt.resolve
 
 plugins {
     alias(libs.plugins.kotlin.jvm)
     alias(libs.plugins.compose.desktop)
     alias(libs.plugins.kotlin.serialization)
+    alias(libs.plugins.gradle.versions)
+//    DesktopPlugin
 //    alias(libs.plugins.kotlin.kapt)
 //    id("dev.datlag.sekret") version "2.0.0-alpha-07"
 //    id("com.bintray.gradle.plugins.bintray")
@@ -77,8 +80,10 @@ allprojects {
         // tts
         implementation(libs.tts)
         // dbus
-        implementation(libs.dbus.java.core)
-        implementation(libs.dbus.java.transport.native.unixsocket)
+//        implementation(libs.dbus.java.core)
+//        implementation(libs.dbus.java.transport.native.unixsocket)
+        // wayland
+        implementation("org.freedesktop:wayland-client:1.5.1")
         // gettext
         implementation("org.gnu.gettext:libintl:0.18.3")
         // paths
@@ -145,62 +150,10 @@ allprojects {
 //        implementation('org.hid4java:hid4java')
         // javascript
         // implementation(libs.mozilla.rhino)
-
-//        implementation("com.github.SmartToolFactory:Compose-Extended-Colors:1.0.0-alpha06")
-//        implementation("com.github.SmartToolFactory:Compose-Extended-Gestures:2.0.0")
 //        implementation ("androidx.compose.ui:ui-text-google-fonts:1.6.8")
     }
 }
 
-inline fun <reified T> Project.configureIfExists(fn: T.() -> Unit) {
-    extensions.findByType(T::class.java)?.fn()
-}
-
-fun Project.createTask(
-    name: String,
-    group: String = "mjdev",
-    description: String = "",
-    configureAction: Task.() -> Unit
-) = tasks.create(name).apply {
-    this.group = group
-    this.description = description
-    configureAction.invoke(this)
-}
-
-//tasks.withType(Copy::class) {
-//    from {
-//        fileTree("native") {
-//            include "*.so", "*.dylib", "*.dll"
-//        }
-//    }
-//    into {
-//        configurations.runtimeClasspath.files.collect { it.absolutePath }
-//    }
-//}
-
-//bintray {
-//    user = 'your_bintray_username'
-//    key = 'your_bintray_api_key'
-//
-//    publish = true
-//    pkg {
-//        repo = 'your_repository_name'
-//        name = project.name
-//        desc = 'Your desktop Compose application'
-//        version = project.version
-//        licenses = ['Apache-2.0']
-//        vcsUrl = 'https://github.com/your_username/your_project'
-//
-//        // Configure deb package dependencies
-//        deb {
-//            packageDescription = 'Your desktop Compose application'
-//            packageVersion = project.version
-//            packageArchitecture = 'amd64'  // Adjust based on your target architecture
-//            packageDepends = ['dbus-java', 'libgtk-3-dev']  // Replace with your actual dependencies
-//        }
-//    }
-//}
-
 compose {
     desktop {
         application {
@@ -244,33 +197,25 @@ compose {
 //                    TargetFormat.Pkg,
                     TargetFormat.Rpm
                 )
-//                buildTypes.release.proguard {
-//                    configurationFiles.from("compose-desktop.pro")
-//                }
+                buildTypes.release.proguard {
+                    configurationFiles.from("compose-desktop.pro")
+                }
 //                appResourcesRootDir.set(project.rootDir.resolve("resources"))
             }
         }
     }
 }
 
-configureIfExists<DesktopExtension> {
-    application {
-        jvmArgs += "--enable-preview"
-    }
-    nativeApplication {
-    }
-}
-
 createTask(
     name = "cleanUp",
     group = "mjdev",
     description = "Task cleans project."
 ) {
-    project.rootDir.resolve(".gradle").deleteRecursively()
-    project.rootDir.resolve("build").deleteRecursively()
-    project.rootDir.resolve("buildSrc").resolve("build").deleteRecursively()
-    project.rootDir.resolve("buildSrc").resolve(".gradle").deleteRecursively()
-    project.rootDir.resolve("packages").deleteRecursively()
+    resolve(".gradle").deleteRecursively()
+    resolve("build").deleteRecursively()
+    resolve("buildSrc").resolve("build").deleteRecursively()
+    resolve("buildSrc").resolve(".gradle").deleteRecursively()
+    resolve("packages").deleteRecursively()
 }
 
 createTask(
@@ -278,21 +223,33 @@ createTask(
     group = "mjdev",
     description = "Task packages project to be published."
 ) {
-//    dependsOn("packageAppImage")
+    dependsOn("packageAppImage")
     dependsOn("packageDeb")
 //    dependsOn("packageDmg")
 //    dependsOn("packageExe")
 //    dependsOn("packageMsi")
 //    dependsOn("packagePkg")
 //    dependsOn("packageRpm")
-
 //    dependsOn("packageReleaseDeb")
 }
 
-createTask(
+createTask<JavaExec>(
     name = "runDesktop",
     group = "mjdev",
-    description = "Task packages project to be published."
+    description = "Run app"
+) {
+    mainClass = "eu.mjdev.desktop.MainKt"
+    classpath = sourceSets.main.get().runtimeClasspath
+    jvmArgs("--enable-preview")
+}
+createTask<JavaExec>(
+    name = "runDebug",
+    group = "mjdev",
+    description = "Run app with debug parameter."
 ) {
-    dependsOn("run")
+    mainClass = "eu.mjdev.desktop.MainKt"
+    classpath = sourceSets.main.get().runtimeClasspath
+    args = arrayListOf("-d")
+    jvmArgs("--enable-preview")
+    // dbus-run-session mutter --wayland --nested
 }

+ 30 - 0
buildSrc/build.gradle.kts

@@ -0,0 +1,30 @@
+repositories {
+    google()
+    mavenCentral()
+    gradlePluginPortal()
+}
+
+plugins {
+    `kotlin-dsl`
+    java
+    `java-gradle-plugin`
+}
+
+dependencies {
+//    implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
+    implementation(gradleApi())
+    implementation(gradleKotlinDsl())
+    implementation(libs.gradle)
+    implementation(libs.gradle.api)
+}
+
+gradlePlugin {
+    plugins {
+        register("DesktopPlugin") {
+            id = "DesktopPlugin"
+            displayName = "DesktopPlugin"
+            description = "Common application plugin to handle all stuffs needed."
+            implementationClass = "org.mjdev.gradle.plugin.DesktopPlugin"
+        }
+    }
+}

+ 7 - 0
buildSrc/settings.gradle.kts

@@ -0,0 +1,7 @@
+dependencyResolutionManagement {
+    versionCatalogs {
+        create("libs") {
+            from(files("../gradle/libs.versions.toml"))
+        }
+    }
+}

+ 70 - 0
buildSrc/src/main/kotlin/org/mjdev/gradle/extensions/DependencyExt.kt

@@ -0,0 +1,70 @@
+package org.mjdev.gradle.extensions
+
+//import org.gradle.api.artifacts.Dependency
+//import org.gradle.api.artifacts.MinimalExternalModuleDependency
+//import org.gradle.api.internal.catalog.ExternalModuleDependencyFactory
+//import org.gradle.api.provider.Provider
+//import org.gradle.kotlin.dsl.DependencyHandlerScope
+
+//object DependencyExt {
+//    private const val IMPLEMENTATION = "implementation"
+//    private const val DEBUG_IMPLEMENTATION = "debugImplementation"
+//    private const val ANDROID_TEST_IMPLEMENTATION = "androidTestImplementation"
+//    private const val TEST_IMPLEMENTATION = "testImplementation"
+//    private const val KAPT_IMPLEMENTATION = "kapt"
+//    private const val KSP_IMPLEMENTATION = "ksp"
+//
+//    fun DependencyHandlerScope.implementation(
+//        dependency: Provider<MinimalExternalModuleDependency>
+//    ) {
+//        add(IMPLEMENTATION, dependency.get())
+//    }
+//
+//    fun DependencyHandlerScope.implementation(
+//        dependency: ExternalModuleDependencyFactory.DependencyNotationSupplier
+//    ) {
+//        add(IMPLEMENTATION, dependency.asProvider().get())
+//    }
+//
+//    fun DependencyHandlerScope.implementation(
+//        dependency: Dependency
+//    ) {
+//        add(IMPLEMENTATION, dependency)
+//    }
+//
+//    fun DependencyHandlerScope.implementation(
+//        dependency: String
+//    ) {
+//        add(IMPLEMENTATION, dependency)
+//    }
+//
+//    fun DependencyHandlerScope.debugImplementation(
+//        dependency: Provider<MinimalExternalModuleDependency>
+//    ) {
+//        add(DEBUG_IMPLEMENTATION, dependency.get())
+//    }
+//
+//    fun DependencyHandlerScope.androidTestImplementation(
+//        dependency: Provider<MinimalExternalModuleDependency>
+//    ) {
+//        add(ANDROID_TEST_IMPLEMENTATION, dependency.get())
+//    }
+//
+//    fun DependencyHandlerScope.testImplementation(
+//        dependency: Provider<MinimalExternalModuleDependency>
+//    ) {
+//        add(TEST_IMPLEMENTATION, dependency.get())
+//    }
+//
+//    fun DependencyHandlerScope.ksp(
+//        dependency: Provider<MinimalExternalModuleDependency>
+//    ) {
+//        add(KSP_IMPLEMENTATION, dependency.get())
+//    }
+//
+//    fun DependencyHandlerScope.kapt(
+//        dependency: Provider<MinimalExternalModuleDependency>
+//    ) {
+//        add(KAPT_IMPLEMENTATION, dependency.get())
+//    }
+//}

+ 91 - 0
buildSrc/src/main/kotlin/org/mjdev/gradle/extensions/ProjectExt.kt

@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.gradle.extensions
+
+//import org.gradle.accessors.dm.LibrariesForLibs
+import org.gradle.api.*
+import org.gradle.internal.impldep.jakarta.xml.bind.DatatypeConverter
+import org.gradle.kotlin.dsl.*
+import java.io.File
+
+@Suppress("unused", "MemberVisibilityCanBePrivate")
+object ProjectExt {
+    val Project.androidStudioVersion
+        get() = project.extra.properties["android.studio.version"]
+
+    val Project.runningFromAndroidStudio
+        get() = DatatypeConverter.parseBoolean(project.extra.properties["android.injected.invoked.from.ide"].toString())
+
+    val Project.isAndroidStudio
+        get() = project.extra.properties.keys.contains("android.studio.version")
+
+//    val Project.libs
+//        get() = the<LibrariesForLibs>()
+
+//    val Project.libs: LibrariesForLibs get() =
+//        (this as ExtensionAware).extensions.getByName("libs") as org.gradle.accessors.dm.LibrariesForLibs
+
+//    fun Project.libs(configure: Action<LibrariesForLibs>): Unit =
+//        (this as org.gradle.api.plugins.ExtensionAware).extensions.configure("libs", configure)
+
+//    val Project.libs : VersionCatalog
+//        get() = rootProject.extensions
+//            .getByType<VersionCatalogsExtension>().named("libs")
+
+//    inline fun <reified T : Plugin<Project>> Project.apply() =
+//        project.plugins.apply(T::class.java)
+
+//    fun Project.apply(plugin: Provider<*>) =
+//        project.plugins.apply(plugin.get().toString())
+
+//    fun Project.apply(plugin: String) =
+//        project.plugins.apply(plugin)
+
+    inline fun <reified T> Project.configureIfExists(fn: T.() -> Unit) {
+        extensions.findByType(T::class.java)?.fn()
+    }
+
+    @Suppress("UNUSED_PARAMETER")
+    inline fun <reified T : Task> Project.createTask(
+        name: String,
+        group: String = "mjdev",
+        description: String = "",
+        className: String? = T::class.simpleName,
+        configureAction: T.() -> Unit
+    ) = tasks.create<T>(name).apply {
+        this.group = group
+        this.description = description
+        configureAction.invoke(this)
+    }
+
+    inline fun Project.createTask(
+        name: String,
+        group: String = "mjdev",
+        description: String = "",
+        configureAction: Task.() -> Unit
+    ) = tasks.create<Task>(name).apply {
+        this.group = group
+        this.description = description
+        configureAction.invoke(this)
+    }
+
+    fun Project.resolve(relative: String): File = rootDir.resolve(File(relative))
+
+//tasks.withType(Copy::class) {
+//    from {
+//        fileTree("native") {
+//            include "*.so", "*.dylib", "*.dll"
+//        }
+//    }
+//    into {
+//        configurations.runtimeClasspath.files.collect { it.absolutePath }
+//    }
+//}
+
+}

+ 28 - 0
buildSrc/src/main/kotlin/org/mjdev/gradle/plugin/DesktopPlugin.kt

@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.gradle.plugin
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+
+class DesktopPlugin : Plugin<Project> {
+    override fun apply(project: Project): Unit = with(project) {
+        println("Main plugin started for $name")
+//        apply(libs.plugins.kotlin.jvm)
+//        apply(libs.plugins.compose.desktop)
+//        apply(libs.plugins.kotlin.serialization)
+//        configureIfExists<DesktopExtension> {
+//            application {
+//                jvmArgs += "--enable-preview"
+//            }
+//            nativeApplication {
+//            }
+//        }
+    }
+}

+ 4 - 1
gradle/libs.versions.toml

@@ -11,7 +11,9 @@ packageName = "org.mjdev.desktop"
 packageVersion = "1.0.1"
 #
 gradle = "8.4.0"
+gradle-versions = "0.51.0"
 kotlin = "1.9.22"
+intellij = "1.17.4"
 compose = "1.7.0-alpha03"
 okhttp3 = "5.0.0-alpha.12"
 coil3 = "3.0.0-alpha06"
@@ -108,4 +110,5 @@ dbus-java-transport-native-unixsocket = { module = "com.github.hypfvieh:dbus-jav
 kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
 kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
 compose-desktop = { id = "org.jetbrains.compose", version.ref = "compose" }
-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
+kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
+gradle-versions = { id = "com.github.ben-manes.versions", version.ref = "gradle-versions" }

+ 10 - 10
src/main/kotlin/eu/mjdev/desktop/Main.kt

@@ -1,22 +1,22 @@
 package eu.mjdev.desktop
 
-import androidx.compose.material.MaterialTheme
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.window.application
+import androidx.compose.runtime.DisposableEffect
 import eu.mjdev.desktop.components.main.MainWindow
-import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
-import eu.mjdev.desktop.provider.DesktopProvider.Companion.rememberDesktopProvider
+import eu.mjdev.desktop.helpers.application.application
+import eu.mjdev.desktop.helpers.system.Shell
 
 fun main(
     args: Array<String>
 ) = application(
+    args = args.toList(),
     exitProcessOnExit = true
 ) {
-    val api = rememberDesktopProvider()
-    println("Started with args: ${args.toList()}")
-    MaterialTheme {
-        CompositionLocalProvider(LocalDesktop provides api) {
-            MainWindow()
+    MainWindow()
+    DisposableEffect(Unit) {
+        println("App started with args: $args")
+        Shell.autoStartApps()
+        onDispose {
+            println("App ended.")
         }
     }
 }

+ 2 - 2
src/main/kotlin/eu/mjdev/desktop/components/controlcenter/ControlCenter.kt

@@ -183,9 +183,9 @@ fun ControlCenter(
                                 }
                             }
                             Divider(
-                                modifier = Modifier.fillMaxHeight().width(controlCenterDividerWidth),
+                                modifier = Modifier.fillMaxHeight().width(2.dp),
                                 color = Color.White.copy(0.1f),
-                                thickness = controlCenterDividerWidth
+                                thickness = 2.dp
                             )
                         }
                     }

+ 21 - 12
src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/DesktopPanel.kt

@@ -75,29 +75,38 @@ fun DesktopPanel(
         if (visible) iconSize.height + iconOuterPadding.height + tooltipHeight + theme.panelContentPadding.times(2)
         else panelDividerWidth
     }
-    val size = remember(panelState.isVisible) {
+    val panelSize = remember(panelState.isVisible, panelState.enabled) {
         DpSize(containerSize.width, panelHeight(panelState.isVisible))
     }
-    val windowState: ChromeWindowState = rememberChromeWindowState(position = position, size = size)
+    val windowState: ChromeWindowState = rememberChromeWindowState(
+        position = position,
+        size = panelSize
+    )
+    panelState.updateSize(panelSize - DpSize(0.dp, tooltipHeight))
     ChromeWindow(
         windowState = windowState,
         visible = true,
+        alwaysOnTop = panelState.enabled,
         enterAnimation = enterAnimation,
         exitAnimation = exitAnimation,
         onFocusChange = { focused ->
             panelState.onFocusChange(focused)
             onFocusChange(focused)
         }) {
-        SlidingMenu(modifier = Modifier.fillMaxWidth(), orientation = Vertical, state = panelState, onPointerEnter = {
-            panelState.show()
-            if (!menuState.isVisible) {
-                windowState.requestFocus()
-            }
-        }, onPointerLeave = {
-            if (!menuState.isVisible) {
-                panelState.hide()
-            }
-        }) { isVisible ->
+        SlidingMenu(
+            modifier = Modifier.fillMaxWidth(),
+            orientation = Vertical,
+            state = panelState,
+            onPointerEnter = {
+                panelState.show()
+                if (!menuState.isVisible) {
+                    windowState.requestFocus()
+                }
+            }, onPointerLeave = {
+                if (!menuState.isVisible) {
+                    panelState.hide()
+                }
+            }) { isVisible ->
             Divider(
                 modifier = Modifier.fillMaxWidth().height(panelDividerWidth),
                 color = Color.Transparent,

+ 4 - 5
src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/DesktopPanelIcon.kt

@@ -5,9 +5,7 @@ import androidx.compose.foundation.layout.*
 import androidx.compose.material.Button
 import androidx.compose.material.ButtonDefaults
 import androidx.compose.material.CircularProgressIndicator
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.*
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.DpSize
@@ -21,6 +19,7 @@ import eu.mjdev.desktop.extensions.Compose.onMouseEnter
 import eu.mjdev.desktop.extensions.Compose.onMouseLeave
 import eu.mjdev.desktop.extensions.Compose.onMousePress
 import eu.mjdev.desktop.extensions.Compose.onRightClick
+import eu.mjdev.desktop.extensions.Compose.rememberCalculated
 import eu.mjdev.desktop.extensions.Compose.rememberState
 import eu.mjdev.desktop.extensions.Modifier.clipRect
 import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
@@ -28,8 +27,6 @@ import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
 @Composable
 fun DesktopPanelIcon(
     app: App? = null,
-    isRunning: Boolean = false,
-    isStarting: Boolean = false,
     icon: String? = null,
     iconColor: Color = Color.Black,
     iconBackgroundColor: Color = Color.White,
@@ -52,6 +49,8 @@ fun DesktopPanelIcon(
         iconState.value -> iconBackgroundHover
         else -> Color.Transparent
     }
+    val isStarting by rememberCalculated(app) { app?.isStarting ?: false }
+    val isRunning by rememberCalculated(app) { app?.isRunning ?: false }
     Box(
         modifier = Modifier
             .size(iconSize)

+ 0 - 2
src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/applets/DesktopPanelFavoriteApps.kt

@@ -44,8 +44,6 @@ fun DesktopPanelFavoriteApps(
             items(apps) { app ->
                 DesktopPanelIcon(
                     app = app,
-                    isStarting = app.isStarting,
-                    isRunning = app.isRunning,
                     iconColor = iconColor,
                     iconBackgroundColor = iconBackgroundColor,
                     iconColorRunning = iconColorRunning,

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/components/gallery/Gallery.kt

@@ -90,7 +90,7 @@ fun Gallery(
                 )
             }
         }
-    ) { _, bckColor, state ->
+    ) { _, bckColor, _ ->
         ImmersiveList(
             modifier = Modifier.fillMaxSize(),
             currentItemIndex = currentItemIndex,

+ 21 - 2
src/main/kotlin/eu/mjdev/desktop/components/image/PhotoImage.kt

@@ -8,12 +8,16 @@
 
 package eu.mjdev.desktop.components.image
 
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.BrokenImage
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.ColorFilter
 import androidx.compose.ui.graphics.DefaultAlpha
@@ -51,9 +55,24 @@ fun PhotoImage(
     if (src == null) {
         placeholder()
     } else {
-        ImageAny (
+        ImageAny(
             src = src,
-            modifier = modifier
+            modifier = modifier.border(
+                BorderStroke(borderSize, borderColor),
+                shape
+            ).background(
+                backgroundColor,
+                shape
+            ).alpha(alpha),
+            alignment = alignment,
+            contentScale = contentScale,
+            alpha = alpha,
+            colorFilter = colorFilter,
+//            filterQuality = filterQuality,
+            placeholder = placeholder,
+            contentDescription = contentDescription,
+            onLoading = {},
+            onFail = {}
         )
 //        GlideImage(
 //            imageModel = { src },

+ 5 - 1
src/main/kotlin/eu/mjdev/desktop/components/main/MainWindow.kt

@@ -19,7 +19,11 @@ import eu.mjdev.desktop.windows.DesktopWindow
 @Composable
 fun MainWindow() = withDesktopScope {
     val controlCenterState = rememberVisibilityState()
-    val panelState = rememberVisibilityState()
+    // todo from user setttings
+    val panelState = rememberVisibilityState(
+        enabled = false,
+        visible = true
+    )
     val menuState = rememberVisibilityState()
     val installWindowSate = rememberVisibilityState()
     val infoWindowSate = rememberVisibilityState(false) //(api.isFirstStart || api.isDebug) // todo

+ 9 - 3
src/main/kotlin/eu/mjdev/desktop/components/sliding/base/VisibilityState.kt

@@ -5,6 +5,7 @@ import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.unit.DpSize
 import eu.mjdev.desktop.helpers.internal.DpBounds
 import eu.mjdev.desktop.helpers.internal.DpBounds.Companion.toDpBounds
 
@@ -64,14 +65,19 @@ class VisibilityState(
         focusState.value = focus
     }
 
+    fun updateSize(panelSize: DpSize) {
+        bounds.height = panelSize.height
+        bounds.width = panelSize.width
+    }
+
     companion object {
         @Composable
         fun rememberVisibilityState(
-            startState: Boolean = false,
+            visible: Boolean = false,
             enabled: Boolean = true,
         ): VisibilityState {
-            return remember(startState, enabled) {
-                VisibilityState(startState, enabled)
+            return remember(visible, enabled) {
+                VisibilityState(visible, enabled)
             }
         }
     }

+ 1 - 2
src/main/kotlin/eu/mjdev/desktop/components/text/TextAny.kt

@@ -2,7 +2,6 @@ package eu.mjdev.desktop.components.text
 
 import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.foundation.text.selection.SelectionContainer
-import androidx.compose.material.LocalTextStyle
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
@@ -37,7 +36,7 @@ fun TextAny(
     minLines: Int = 1,
     maxLines: Int = Int.MAX_VALUE,
     onTextLayout: ((TextLayoutResult) -> Unit)? = null,
-    style: TextStyle = LocalTextStyle.current,
+    style: TextStyle = TextStyle.Default,
     textSelectionEnabled: Boolean = false
 ) {
     val content: @Composable () -> Unit = {

+ 3 - 1
src/main/kotlin/eu/mjdev/desktop/components/tooltip/Tooltip.kt

@@ -51,7 +51,9 @@ fun Tooltip(
                     TextAny(
                         text = description,
                         color = textColor,
-                        fontSize = 12.sp
+                        fontSize = 12.sp,
+                        minLines = 1,
+                        maxLines = 2
                     )
                 }
             }

+ 50 - 59
src/main/kotlin/eu/mjdev/desktop/data/App.kt

@@ -6,9 +6,6 @@ import androidx.compose.ui.graphics.Color
 import eu.mjdev.desktop.helpers.exception.EmptyException.Companion.EmptyException
 import eu.mjdev.desktop.helpers.system.Shell
 import eu.mjdev.desktop.provider.DesktopProvider
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
 import java.io.File
 
 @Suppress("unused", "MemberVisibilityCanBePrivate")
@@ -19,6 +16,8 @@ class App(
 ) {
     val fileName
         get() = desktopFile?.fileName
+    val fullAppName
+        get() = desktopFile?.fullAppName ?: ""
     val type
         get() = desktopFile?.desktopSection?.Type ?: ""
     val version
@@ -39,6 +38,8 @@ class App(
         get() = desktopFile?.desktopSection?.NotifyOnStart ?: false
     val runInTerminal
         get() = desktopFile?.desktopSection?.RunInTerminal ?: false
+    val windowClass
+        get() = desktopFile?.desktopSection?.StartupWMClass.orEmpty().ifEmpty { fullAppName }
 
     var iconTint: Color? = null
     val iconBackground: Color = Color.White
@@ -47,56 +48,39 @@ class App(
     val onStartingHandler: MutableState<() -> Unit> = mutableStateOf({})
     val onStartedHandler: MutableState<() -> Unit> = mutableStateOf({})
 
-    var process: Shell? = null
-
     var isStartingState = mutableStateOf(false)
+    var isRunningState = mutableStateOf(false)
     var isStarting: Boolean
         get() = isStartingState.value
-        set(value) {
+        internal set(value) {
             isStartingState.value = value
         }
-
-    val isRunningState = mutableStateOf(false)
     var isRunning: Boolean
         get() = isStartingState.value
-        set(value) {
+        internal set(value) {
             isStartingState.value = value
         }
 
-    // todo :  replace all params with what needed
-    val cmd
-        get() = exec.replace("%u", "").replace("%U", "").trim()
-
-    val command
-        get() = process?.command
-
-    fun start(
-        api: DesktopProvider,
-        scope: CoroutineScope = api.scope
-    ) = runCatching {
-        scope.launch {
-            println("Starting app: $name [$fileName].")
-            triggerStart()
-            process = Shell(api)
-                .writeCommand("export DBUS_SESSION_BUS_ADDRESS=\"unix:path=\$XDG_RUNTIME_DIR/bus\"")
-                .writeCommand("$cmd  &")
-                .writeCommand("exit")
-                .apply {
-                    triggerStarted()
-                    println("app started.")
-                    while (this.isRunning) {
-                        delay(100)
-                    }
-                    triggerStop()
-                    println("app stopped")
-                }
-        }
-    }.onFailure { error ->
-        triggerStop(error)
+    fun start() = runCatching {
+        println("Starting app: $name [$windowClass].")
+        println("dex -w ${desktopFile?.absolutePath}")
+        triggerStart()
+        Shell.startApp(
+            app = this@App,
+            onStarted = {
+                println("App started app: $name [$windowClass].")
+                triggerStarted()
+            },
+            onStopped = { e ->
+                println("App stopped: $name [$windowClass].")
+                triggerStop(e)
+            }
+        )
+    }.onFailure { e ->
+        triggerStop(e)
     }
 
     fun triggerStart() {
-        isRunning = false
         isStarting = true
         onStartingHandler.value.invoke()
     }
@@ -130,10 +114,6 @@ class App(
         return this
     }
 
-    fun exit() {
-        process?.close()
-    }
-
     override fun equals(other: Any?): Boolean {
         return when {
             other is App -> {
@@ -144,25 +124,36 @@ class App(
         }
     }
 
+    fun hasWindow(api: DesktopProvider): Boolean =
+        api.windowsManager.hasWindow(this)
+
     override fun hashCode(): Int {
-        var result = file.hashCode()
-        result = 31 * result + desktopFile.hashCode()
-        result = 31 * result + isRunning.hashCode()
+        var result = file?.hashCode() ?: 0
+        result = 31 * result + (desktopFile?.hashCode() ?: 0)
+        result = 31 * result + isFavorite.hashCode()
+        result = 31 * result + (fileName?.hashCode() ?: 0)
+        result = 31 * result + (fullAppName.hashCode())
+        result = 31 * result + type.hashCode()
+        result = 31 * result + version.hashCode()
+        result = 31 * result + name.hashCode()
+        result = 31 * result + comment.hashCode()
+        result = 31 * result + path.hashCode()
+        result = 31 * result + exec.hashCode()
+        result = 31 * result + iconName.hashCode()
+        result = 31 * result + categories.hashCode()
+        result = 31 * result + notifyOnStart.hashCode()
+        result = 31 * result + runInTerminal.hashCode()
+        result = 31 * result + (windowClass.hashCode())
+        result = 31 * result + (iconTint?.hashCode() ?: 0)
+        result = 31 * result + iconBackground.hashCode()
+        result = 31 * result + onStopHandler.hashCode()
+        result = 31 * result + onStartingHandler.hashCode()
+        result = 31 * result + onStartedHandler.hashCode()
+        result = 31 * result + isStartingState.hashCode()
+        result = 31 * result + isStarting.hashCode()
         return result
     }
 
-//    fun hasWindow(api: DesktopProvider): Boolean =
-//        process?.hasWindow(api) ?: false
-
-//    fun isWindowFocus(api: DesktopProvider): Boolean =
-//        process?.isWindowFocus(api) ?: false
-
-//    fun minimizeWindow(api: DesktopProvider) =
-//        process?.minimizeWindow(api) ?: false
-
-//    fun requestWindowFocus(api: DesktopProvider) =
-//        process?.requestWindowFocus(api) ?: false
-
     companion object {
         val Empty: App = App()
 

+ 89 - 34
src/main/kotlin/eu/mjdev/desktop/data/DesktopFile.kt

@@ -3,6 +3,7 @@ package eu.mjdev.desktop.data
 import eu.mjdev.desktop.extensions.Custom.ParsedBoolean
 import eu.mjdev.desktop.extensions.Custom.ParsedList
 import eu.mjdev.desktop.extensions.Custom.ParsedString
+import eu.mjdev.desktop.helpers.system.LinuxIni
 import org.ini4j.Ini
 import org.ini4j.Profile.Section
 import org.ini4j.Wini
@@ -10,14 +11,24 @@ import java.io.File
 
 @Suppress("unused", "MemberVisibilityCanBePrivate", "ConstPropertyName", "PropertyName")
 class DesktopFile(
-    val file: File,
-    val content: Ini = runCatching { Wini(file) }.getOrNull() ?: Wini(),
+    var file: File,
+    val correctDir: String = "/tmp/.corrected-desktop-files/",
+    val correctedFile: File = File(File(correctDir), file.name),
+    var fileData: String = (if (correctedFile.exists()) correctedFile else file).readText(),
+    var content: Ini = (runCatching { LinuxIni(fileData) }.getOrNull() ?: LinuxIni())
 ) {
-    var fileName: String = file.name
-    val absolutePath: String = file.absolutePath
+    var fileName: String = if (correctedFile.exists()) correctedFile.name else file.name
+    val absolutePath: String = if (correctedFile.exists()) correctedFile.absolutePath else file.absolutePath
+
+    val fullAppName
+        get() = if (correctedFile.exists()) correctedFile.nameWithoutExtension else file.nameWithoutExtension
 
     val sections: Map<String, Section>
-        get() = content.map { if (it is Section) it else null }.filterNotNull().associateBy { it.value.toString() }
+        get() = content.map {
+            if (it is Section) it else null
+        }.filterNotNull().associateBy {
+            it.value.toString()
+        }
 
     val desktopSection
         get() = content[DesktopFileType.DesktopEntry.text]?.let { DesktopSectionScope(it) }
@@ -25,6 +36,30 @@ class DesktopFile(
     val themeSection
         get() = content[DesktopFileType.Theme.text]?.let { ThemeSectionScope(it) }
 
+    val isApp
+        get() = desktopSection?.Type == DesktopFileType.Application
+
+    val isCorrect
+        get() = correctedFile.exists() || fileData.contains(Prop_StartupWMClass)
+
+    init {
+        if (isApp && !isCorrect) {
+            runCatching {
+                println("Correcting file: file:///$absolutePath")
+                println("Corrected file: file:///${correctedFile.absolutePath}")
+                correctedFile.let { f ->
+                    desktopSection?.StartupWMClass = fullAppName
+                    writeTo(f)
+                    file = f
+                    fileData = file.readText()
+                    content = Wini(f)
+                }
+            }.onFailure { e ->
+                e.printStackTrace()
+            }
+        }
+    }
+
     fun section(
         type: DesktopFileType,
         block: Section.() -> Unit
@@ -42,14 +77,42 @@ class DesktopFile(
         ThemeSectionScope(this).apply(block)
     }
 
-    fun write() {
-        if (file.exists()) {
-            file.delete()
-        }
+    fun mkDirs(): DesktopFile {
+        if (!file.parentFile.exists()) file.parentFile.mkdirs()
+        return this
+    }
+
+    fun deleteFile(): DesktopFile {
+        if (file.exists()) file.delete()
+        return this
+    }
+
+    fun createNewFile(): DesktopFile {
         file.createNewFile()
-        content.store(file)
+        return this
+    }
+
+    fun newFile(): DesktopFile {
+        mkDirs()
+        deleteFile()
+        createNewFile()
+        return this
+    }
+
+    fun writeTo(f: File) {
+        if (!f.parentFile.exists()) {
+            f.parentFile.mkdirs()
+        }
+        if (f.exists()) {
+            f.delete()
+        }
+        f.createNewFile()
+        content.store(f)
     }
 
+    fun write() =
+        writeTo(file)
+
     interface ISectionScope {
         val section: Section
     }
@@ -120,6 +183,12 @@ class DesktopFile(
             set(value) {
                 section[Prop_Categories] = value.joinToString { "$it;" }
             }
+        var StartupWMClass: String
+            get() = ParsedString(section[Prop_StartupWMClass]).trim()
+            set(value) {
+                println("setting [$Prop_StartupWMClass] = $StartupWMClass")
+                section[Prop_StartupWMClass] = value
+            }
     }
 
     class ThemeSectionScope(
@@ -177,28 +246,6 @@ class DesktopFile(
         }
     }
 
-    fun mkDirs(): DesktopFile {
-        if (!file.parentFile.exists()) file.parentFile.mkdirs()
-        return this
-    }
-
-    fun deleteFile(): DesktopFile {
-        if (file.exists()) file.delete()
-        return this
-    }
-
-    fun createNewFile(): DesktopFile {
-        file.createNewFile()
-        return this
-    }
-
-    fun newFile(): DesktopFile {
-        mkDirs()
-        deleteFile()
-        createNewFile()
-        return this
-    }
-
     companion object {
         const val Prop_Type = "Type"
         const val Prop_Version = "Version"
@@ -210,17 +257,25 @@ class DesktopFile(
         const val Prop_Terminal = "Terminal"
         const val Prop_Icon = "Icon"
         const val Prop_Categories = "Categories"
+        const val Prop_StartupWMClass = "StartupWMClass"
+
+        // todo
         const val Prop_GenericName = "GenericName"
         const val Prop_MimeType = "MimeType"
         const val Prop_Actions = "Actions"
         const val Prop_DBusActivatable = "DBusActivatable"
         const val Prop_Encoding = "Encoding"
-
+        const val Prop_Keywords = "Keywords"
+        const val Prop_OnlyShowIn = "OnlyShowIn"
+        const val Prop_X_ExecArg = "X-ExecArg"
+        const val Prop_X_Ubuntu_Gettext_Domain = "X-Ubuntu-Gettext-Domain"
+        const val Prop_NoDisplay = "NoDisplay" // boolean
         const val Prop_X_GNOME_UsesNotifications = "X-GNOME-UsesNotifications"
         const val Prop_X_Purism_FormFactor = "X-Purism-FormFactor"
         const val Prop_X_Unity_IconBackgroundColor = "X-Unity-IconBackgroundColor"
-        const val Prop_X_Ubuntu_Gettext_Domain = "X-Ubuntu-Gettext-Domain"
+        const val Prop_SingleMainWindow = "SingleMainWindow"
 
+        // theme
         const val Prop_GtkTheme = "GtkTheme"
         const val Prop_MetacityTheme = "MetacityTheme"
         const val Prop_IconTheme = "IconTheme"

+ 6 - 0
src/main/kotlin/eu/mjdev/desktop/extensions/Compose.kt

@@ -158,6 +158,12 @@ object Compose {
         calculation: () -> T
     ) = remember(*key) { derivedStateOf { calculation() } }
 
+    @Composable
+    fun <T> rememberComputed(
+        vararg key: Any?,
+        calculation: () -> T
+    ) = remember(*key) { derivedStateOf { calculation() } }
+
     @OptIn(ExperimentalComposeUiApi::class)
     fun Modifier.onMouseEnter(
         pass: PointerEventPass = PointerEventPass.Main,

+ 7 - 19
src/main/kotlin/eu/mjdev/desktop/extensions/Custom.kt

@@ -19,26 +19,11 @@ import kotlin.jvm.optionals.getOrNull
 
 @Suppress("FunctionName", "MemberVisibilityCanBePrivate", "unused")
 object Custom {
+    val ProcessHandle.command :String
+        get() = info().command().getOrNull().orEmpty()
 
-    val Process.parentPid: Long?
-        get() = toHandle().parentPid
-
-    val Process.childs
-        get() = toHandle().childs
-
-    val ProcessHandle.parentPid: Long?
-        get() = parent().getOrNull()?.pid()
-
-    val ProcessHandle.childs: List<ProcessHandle>
-        get() = getProcessChilds()
-
-    fun ProcessHandle.getProcessChilds(): List<ProcessHandle> = ProcessHandle.allProcesses().filter { ph ->
-        ph.parentPid == pid()
-    }.toList().toMutableList().apply {
-        forEach { ph ->
-            addAll(ph.getProcessChilds())
-        }
-    }
+    val ProcessHandle.commandLine :String
+        get() = info().commandLine().getOrNull().orEmpty()
 
     fun BufferedReader.readAvailable(): String = StringBuilder().apply {
         var ch = read()
@@ -48,6 +33,9 @@ object Custom {
         }
     }.toString()
 
+    inline fun <reified T> Any?.orElse(block: () -> T) =
+        if (this == null) block() else this as T
+
     fun ParsedList(
         value: String?,
         delimiter: String = ";"

+ 93 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/application/Application.kt

@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.helpers.application
+
+import androidx.compose.runtime.*
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.configureSwingGlobalsForCompose
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.LocalLocalization
+import eu.mjdev.desktop.helpers.application.base.GlobalDensity
+import eu.mjdev.desktop.helpers.application.base.GlobalLayoutDirection
+import eu.mjdev.desktop.helpers.application.base.GlobalSnapshotManager
+import eu.mjdev.desktop.helpers.application.base.YieldFrameClock
+import eu.mjdev.desktop.helpers.application.l10n.defaultPlatformLocalization
+import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
+import eu.mjdev.desktop.provider.DesktopProvider.Companion.rememberDesktopProvider
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.jetbrains.skiko.MainUIDispatcher
+import kotlin.system.exitProcess
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun application(
+    args: List<String> = emptyList(),
+    exitProcessOnExit: Boolean = true,
+    content: @Composable ApplicationScope.() -> Unit
+) {
+    if (System.getProperty("compose.application.configure.swing.globals") == "true") {
+        configureSwingGlobalsForCompose()
+    }
+    runBlocking {
+        awaitApplication(args) {
+            content()
+        }
+    }
+    if (exitProcessOnExit) {
+        exitProcess(0)
+    }
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+suspend fun awaitApplication(
+    args: List<String> = emptyList(),
+    content: @Composable ApplicationScope.() -> Unit
+) {
+    val applicationScope = object : ApplicationScope(args) {
+        override fun exitApplication() {
+            isOpen = false
+        }
+    }
+    if (System.getProperty("compose.application.configure.swing.globals") == "true") {
+        configureSwingGlobalsForCompose()
+    }
+    withContext(MainUIDispatcher) {
+        withContext(YieldFrameClock) {
+            GlobalSnapshotManager.ensureStarted()
+            val recomposer = Recomposer(coroutineContext)
+            launch {
+                recomposer.runRecomposeAndApplyChanges()
+            }
+            launch {
+                val applier = ApplicationApplier()
+                val composition = Composition(applier, recomposer)
+                try {
+                    composition.setContent {
+                        if (applicationScope.isOpen) {
+                            CompositionLocalProvider(
+                                LocalDensity provides GlobalDensity,
+                                LocalLayoutDirection provides GlobalLayoutDirection,
+                                LocalLocalization providesDefault defaultPlatformLocalization(),
+                                LocalDesktop provides rememberDesktopProvider()
+                            ) {
+                                applicationScope.content()
+                            }
+                        }
+                    }
+                    recomposer.close()
+                    recomposer.join()
+                } finally {
+                    composition.dispose()
+                }
+            }
+        }
+    }
+}

+ 23 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/application/ApplicationApplier.kt

@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.helpers.application
+
+import androidx.compose.runtime.Applier
+
+class ApplicationApplier : Applier<Unit> {
+    override val current: Unit = Unit
+    override fun down(node: Unit) = Unit
+    override fun up() = Unit
+    override fun insertTopDown(index: Int, instance: Unit) = Unit
+    override fun insertBottomUp(index: Int, instance: Unit) = Unit
+    override fun remove(index: Int, count: Int) = Unit
+    override fun move(from: Int, to: Int, count: Int) = Unit
+    override fun clear() = Unit
+    override fun onEndChanges() = Unit
+}

+ 79 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/application/ApplicationScope.kt

@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.helpers.application
+
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.*
+import eu.mjdev.desktop.extensions.Compose.launchedEffect
+import eu.mjdev.desktop.helpers.application.block.SCOPED_BLOCK
+import eu.mjdev.desktop.helpers.application.block.invokeAllOnce
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlin.system.exitProcess
+
+@Suppress("MemberVisibilityCanBePrivate", "unused")
+@Stable
+open class ApplicationScope(
+    val args: List<String> = emptyList()
+) {
+    private val isOpenState = mutableStateOf(true)
+    private val sessionStartHandlers = mutableListOf<SCOPED_BLOCK<ApplicationScope>>()
+    private val sessionEndHandlers = mutableListOf<SCOPED_BLOCK<ApplicationScope>>()
+
+    var isOpen
+        get() = isOpenState.value
+        set(value) {
+            isOpenState.value = value
+        }
+
+    fun dispatchStart() = CoroutineScope(Dispatchers.IO).launch {
+        sessionStartHandlers.invokeAllOnce(this@ApplicationScope)
+    }
+
+    fun dispatchEnd() = CoroutineScope(Dispatchers.IO).launch {
+        sessionEndHandlers.invokeAllOnce(this@ApplicationScope)
+    }
+
+    open fun exitApplication() {
+        exitApplication(0)
+    }
+
+    fun exitApplication(code: Int) {
+        exitProcess(code)
+    }
+
+    @Composable
+    fun onSessionStart(
+        block: ApplicationScope.() -> Unit
+    ) {
+        sessionStartHandlers.add(block)
+        LaunchedEffect(sessionStartHandlers.size) {
+            dispatchStart()
+        }
+    }
+
+    @Composable
+    fun onSessionEnd(
+        block: ApplicationScope.() -> Unit
+    ) {
+        sessionEndHandlers.add(block)
+    }
+
+    @Composable
+    fun render(
+        onContentRendered: () -> Unit = {},
+        content: @Composable ApplicationScope.() -> Unit
+    ) = MaterialTheme {
+        content()
+        launchedEffect {
+            onContentRendered()
+        }
+    }
+}

+ 42 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/application/base/GlobalSnapshotManager.kt

@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.helpers.application.base
+
+import java.util.concurrent.atomic.AtomicLong
+import androidx.compose.runtime.snapshots.Snapshot
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.consumeEach
+import kotlinx.coroutines.launch
+import org.jetbrains.skiko.MainUIDispatcher
+
+val GlobalSnapshotManagerDispatcher: CoroutineDispatcher = MainUIDispatcher
+
+object GlobalSnapshotManager {
+    private val started = AtomicLong(0)
+    private val sent = AtomicLong(0)
+
+    fun ensureStarted() {
+        if (started.compareAndSet(0, 1)) {
+            val channel = Channel<Unit>(1)
+            CoroutineScope(GlobalSnapshotManagerDispatcher).launch {
+                channel.consumeEach {
+                    sent.compareAndSet(1, 0)
+                    Snapshot.sendApplyNotifications()
+                }
+            }
+            Snapshot.registerGlobalWriteObserver {
+                if (sent.compareAndSet(0, 1)) {
+                    channel.trySend(Unit)
+                }
+            }
+        }
+    }
+}

+ 41 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/application/base/Helpers.kt

@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.helpers.application.base
+
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import java.awt.ComponentOrientation
+import java.awt.GraphicsConfiguration
+import java.awt.GraphicsEnvironment
+import java.util.*
+
+val GraphicsConfiguration.density: Density
+    get() = Density(
+        defaultTransform.scaleX.toFloat(),
+        fontScale = 1f
+    )
+
+val GlobalDensity
+    get() = GraphicsEnvironment.getLocalGraphicsEnvironment()
+        .defaultScreenDevice
+        .defaultConfiguration
+        .density
+
+val ComponentOrientation.layoutDirection: LayoutDirection
+    get() = when {
+        isLeftToRight -> LayoutDirection.Ltr
+        isHorizontal -> LayoutDirection.Rtl
+        else -> LayoutDirection.Ltr
+    }
+
+val Locale.layoutDirection: LayoutDirection
+    get() = ComponentOrientation.getOrientation(this).layoutDirection
+
+val GlobalLayoutDirection
+    get() = Locale.getDefault().layoutDirection

+ 21 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/application/base/YieldFrameClock.kt

@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.helpers.application.base
+
+import androidx.compose.runtime.MonotonicFrameClock
+import kotlinx.coroutines.yield
+
+object YieldFrameClock : MonotonicFrameClock {
+    override suspend fun <R> withFrameNanos(
+        onFrame: (frameTimeNanos: Long) -> R
+    ): R {
+        yield()
+        return onFrame(System.nanoTime())
+    }
+}

+ 27 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/application/block/Block.kt

@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+@file:Suppress("unused")
+
+package eu.mjdev.desktop.helpers.application.block
+
+typealias BLOCK = () -> Unit
+
+typealias SCOPED_BLOCK<T> = T.() -> Unit
+
+fun <B : BLOCK> List<B>.invokeAll() = forEach { block -> block.invoke() }
+
+fun <S> MutableList<SCOPED_BLOCK<S>>.invokeAll(scope: S) = forEach { block -> block.invoke(scope) }
+
+fun <S> MutableList<SCOPED_BLOCK<S>>.invokeAllOnce(scope: S) = iterator().apply {
+    while (hasNext()) {
+        val block = next()
+        block.invoke(scope)
+        remove()
+    }
+}

+ 49 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/application/l10n/En.kt

@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.helpers.application.l10n
+
+@Suppress("UnusedReceiverParameter", "DuplicatedCode")
+fun Translations.en() = mapOf(
+    Strings.SelectAll to "Select all",
+    Strings.Cut to "Cut",
+    Strings.Copy to "Copy",
+    Strings.Paste to "Paste",
+)
+
+@Suppress("UnusedReceiverParameter", "DuplicatedCode")
+fun Translations.enAU() = mapOf(
+    Strings.SelectAll to "Select all",
+    Strings.Cut to "Cut",
+    Strings.Copy to "Copy",
+    Strings.Paste to "Paste",
+)
+
+@Suppress("UnusedReceiverParameter", "DuplicatedCode")
+fun Translations.enCA() = mapOf(
+    Strings.SelectAll to "Select all",
+    Strings.Cut to "Cut",
+    Strings.Copy to "Copy",
+    Strings.Paste to "Paste",
+)
+
+@Suppress("UnusedReceiverParameter", "DuplicatedCode")
+fun Translations.enGB() = mapOf(
+    Strings.SelectAll to "Select all",
+    Strings.Cut to "Cut",
+    Strings.Copy to "Copy",
+    Strings.Paste to "Paste",
+)
+
+@Suppress("UnusedReceiverParameter", "DuplicatedCode")
+fun Translations.enIN() = mapOf(
+    Strings.SelectAll to "Select all",
+    Strings.Cut to "Cut",
+    Strings.Copy to "Copy",
+    Strings.Paste to "Paste",
+)

+ 65 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/application/l10n/Strings.kt

@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.helpers.application.l10n
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.ui.text.intl.Locale
+
+@Immutable
+@JvmInline
+value class Strings private constructor(@Suppress("unused") private val value: Int) {
+    companion object {
+        val Copy = Strings(0)
+        val Cut = Strings(1)
+        val Paste = Strings(2)
+        val SelectAll = Strings(3)
+        // When adding values here, make sure to also add them in ui/build.gradle,
+        // updateTranslations task (stringByResourceName parameter), and re-run the task
+    }
+}
+
+@Composable
+@ReadOnlyComposable
+fun getString(string: Strings): String {
+    val locale = Locale.current
+    val tag = localeTag(language = locale.language, region = locale.region)
+    val translation = translationByLocaleTag.getOrPut(tag) {
+        findTranslation(locale)
+    }
+    return translation[string] ?: error("Missing translation for $string")
+}
+
+typealias Translation = Map<Strings, String>
+
+val translationByLocaleTag = mutableMapOf<String, Translation>()
+
+fun localeTag(language: String, region: String) = when {
+    language == "" -> ""
+    region == "" -> language
+    else -> "${language}_$region"
+}
+
+fun localeTagChain(locale: Locale) = sequence {
+    if (locale.region != "") {
+        yield(localeTag(language = locale.language, region = locale.region))
+    }
+    if (locale.language != "") {
+        yield(localeTag(language = locale.language, region = ""))
+    }
+    yield(localeTag("", ""))
+}
+
+fun findTranslation(locale: Locale): Map<Strings, String> {
+    // We don't need to merge translations because each one should contain all the strings.
+    return localeTagChain(locale).firstNotNullOf { translationFor(it) }
+}
+
+object Translations

+ 35 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/application/l10n/Translations.kt

@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.helpers.application.l10n
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.PlatformLocalization
+
+fun translationFor(localeTag: String) = when(localeTag) {
+    "" -> Translations.en()
+    "en_AU" -> Translations.enAU()
+    "en_CA" -> Translations.enCA()
+    "en_GB" -> Translations.enGB()
+    "en_IN" -> Translations.enIN()
+    else -> null
+}
+
+@Composable
+fun defaultPlatformLocalization(): PlatformLocalization {
+    val copy = getString(Strings.Copy)
+    val cut = getString(Strings.Cut)
+    val paste = getString(Strings.Paste)
+    val selectAll = getString(Strings.SelectAll)
+    return object : PlatformLocalization {
+        override val copy = copy
+        override val cut = cut
+        override val paste = paste
+        override val selectAll = selectAll
+    }
+}

+ 13 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/exception/ErrorException.kt

@@ -0,0 +1,13 @@
+package eu.mjdev.desktop.helpers.exception
+
+@Suppress("unused")
+class ErrorException(
+    message: String = "-",
+    e: Throwable? = null
+) : Exception(message, e) {
+    companion object {
+        fun error(message: String) = ErrorException(message)
+        fun error(e: Throwable) = ErrorException(e = e)
+        fun error(message: String, e: Throwable) = ErrorException(message, e)
+    }
+}

+ 13 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/exception/SuccessException.kt

@@ -0,0 +1,13 @@
+package eu.mjdev.desktop.helpers.exception
+
+@Suppress("unused")
+class SuccessException(
+    message: String = "-",
+    e: Throwable? = null
+) : Exception(message, e) {
+    companion object {
+        fun success(message: String) = SuccessException(message)
+        fun success(e: Throwable) = SuccessException(e = e)
+        fun success(message: String, e: Throwable) = SuccessException(message, e)
+    }
+}

+ 4 - 4
src/main/kotlin/eu/mjdev/desktop/helpers/internal/DpBounds.kt

@@ -6,10 +6,10 @@ import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 
 data class DpBounds(
-    val x: Dp,
-    val y: Dp,
-    val width: Dp,
-    val height: Dp
+    var x: Dp,
+    var y: Dp,
+    var width: Dp,
+    var height: Dp
 ) {
     companion object {
         val Zero = DpBounds(0.dp, 0.dp, 0.dp, 0.dp)

+ 2 - 2
src/main/kotlin/eu/mjdev/desktop/helpers/internal/Palette.kt

@@ -10,8 +10,8 @@ import eu.mjdev.desktop.extensions.ColorUtils.lighter
 import eu.mjdev.desktop.extensions.ColorUtils.nonAlphaValue
 import eu.mjdev.desktop.extensions.Compose.SuperDarkGray
 import eu.mjdev.desktop.extensions.Image.loadPicture
-import eu.mjdev.desktop.helpers.managers.GnomeManager
-import eu.mjdev.desktop.helpers.managers.GnomeManager.Companion.THEME_MJDEV
+import eu.mjdev.desktop.managers.GnomeManager
+import eu.mjdev.desktop.managers.GnomeManager.Companion.THEME_MJDEV
 import eu.mjdev.desktop.provider.DesktopProvider
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch

+ 4 - 4
src/main/kotlin/eu/mjdev/desktop/helpers/system/Environment.kt

@@ -1,10 +1,10 @@
 package eu.mjdev.desktop.helpers.system
 
 class Environment(
-    val data : List<String> = Shell.executeAndReadLines("env")
-) : ArrayList<String>() {
+    val data: List<String> = Shell.executeAndReadLines("env")
+) : HashMap<String, String>() {
     init {
-        addAll(data)
-        add("export DBUS_SESSION_BUS_ADDRESS=\"unix:path=\$XDG_RUNTIME_DIR/bus\"")
+        putAll(System.getenv())
+        put("DBUS_SESSION_BUS_ADDRESS", "unix:path=\$XDG_RUNTIME_DIR/bus")
     }
 }

+ 37 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/system/LinuxIni.kt

@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+@file:Suppress("DEPRECATION")
+
+package eu.mjdev.desktop.helpers.system
+
+import org.ini4j.Config
+import org.ini4j.Wini
+import java.io.File
+import java.io.FileInputStream
+import java.io.InputStream
+import java.io.StringBufferInputStream
+import java.nio.charset.Charset
+
+@Suppress("unused")
+class LinuxIni(
+    stream: InputStream
+) : Wini(stream) {
+    constructor() : this("")
+    constructor(data: String) : this(StringBufferInputStream(data))
+    constructor(file: File) : this(FileInputStream(file))
+
+    init {
+        config = Config().apply {
+            fileEncoding = Charset.defaultCharset()
+            isPropertyFirstUpper = true
+            isEscape = false
+            isStrictOperator = true
+        }
+    }
+}

+ 81 - 99
src/main/kotlin/eu/mjdev/desktop/helpers/system/Shell.kt

@@ -1,109 +1,91 @@
 package eu.mjdev.desktop.helpers.system
 
-import eu.mjdev.desktop.extensions.Custom.childs
-import eu.mjdev.desktop.extensions.Custom.command
-import eu.mjdev.desktop.provider.DesktopProvider
+import eu.mjdev.desktop.data.App
+import eu.mjdev.desktop.helpers.exception.ErrorException.Companion.error
+import eu.mjdev.desktop.helpers.exception.SuccessException.Companion.success
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
 import java.io.BufferedReader
-import java.io.BufferedWriter
-
-@Suppress("MemberVisibilityCanBePrivate", "CanBeParameter", "unused")
-class Shell(
-    val api: DesktopProvider,
-    val shellType: ShellType = ShellType.SH,
-) {
-    val environment: Environment = Environment()
-    val process: Process = Runtime.getRuntime().exec(
-        arrayOf(shellType.cmd),
-        environment.toTypedArray()
-    )
-    val input: BufferedReader = process.inputReader()
-    val output: BufferedWriter = process.outputWriter()
-
-//    val processInfo
-//        get() = process.info()
-
-    val isAlive: Boolean
-        get() = process.isAlive
-
-    val isRunning:Boolean
-        get() = false // todo : implement process is running monitor
-
-    val hasChilds
-        get() = childProcesses.isNotEmpty()
-
-    val pid
-        get() = process.pid()
-
-    val command: String
-        get() = process.command ?: ""
-
-    val allProcesses: List<ProcessHandle>
-        get() = ProcessHandle.allProcesses().toList()
-
-    val allProcessesPids
-        get() = allProcesses.map { it.pid() }
-
-    val childProcesses: List<ProcessHandle>
-        get() = process.descendants().toList() + process.childs
-
-    fun toHandle(): ProcessHandle =
-        process.toHandle()
-
-    fun writeCommand(cmd: String): Shell {
-        output.write(cmd + "\n")
-        output.flush()
-        return this
-    }
-
-    fun writeCommand(cmd: String, vararg args: String): Shell {
-        output.write(cmd + args.joinToString { " $it " } + "\n")
-        output.flush()
-        return this
-    }
-
-    fun readOutputLine(): String =
-        input.readLine()
-
-    fun readOutputLines(): List<String> = input.readLines().filter {
-        it.isNotEmpty()
+import java.util.*
+
+private val Process.consoleOutput: String
+    get() {
+        val sj = StringJoiner(System.lineSeparator())
+        val bfr = BufferedReader(inputReader())
+        bfr.lines().iterator().forEachRemaining { s: String? -> sj.add(s) }
+        return sj.toString()
     }
 
-    fun readWhileDone(): Shell {
-        process.waitFor()
-        return this
+@Suppress("MemberVisibilityCanBePrivate", "unused")
+object Shell {
+
+    val environment: Environment
+        get() = Environment()
+
+    val Process.handle: ProcessHandle
+        get() = toHandle()
+
+    fun autoStartApps() = CoroutineScope(Dispatchers.IO).launch {
+        ProcessBuilder("dex", "-a").apply {
+            environment().putAll(environment)
+        }.also { processBuilder ->
+            runCatching {
+                processBuilder.start()
+            }.onFailure { e ->
+                e.printStackTrace()
+            }
+        }
     }
 
-    fun close(): Shell {
-        process.destroy()
-        return this
+    fun startApp(
+        app: App,
+        onStarted: () -> Unit = {},
+        onStopped: (e: Throwable) -> Unit = {},
+    ) = ProcessBuilder(
+        "dex",
+        "-w",
+        app.desktopFile?.absolutePath
+    ).apply {
+        environment().putAll(environment)
+    }.also { processBuilder ->
+        runCatching<Process> {
+            processBuilder.start().let { p ->
+                onStarted()
+                p.waitFor()
+                p
+            }
+        }.onFailure { e ->
+            onStopped(error(e))
+        }.onSuccess { p ->
+            if (p.exitValue() == 0) {
+                onStopped(success(p.consoleOutput))
+            } else {
+                onStopped(error(p.consoleOutput))
+            }
+        }
     }
 
-    companion object {
-        fun executeAndRead(cmd: String, vararg args: String): String =
-            Runtime.getRuntime().exec(
-                mutableListOf<String>().apply {
-                    add(cmd)
-                    addAll(args)
-                }.toTypedArray()
-            ).apply {
-                waitFor()
-            }.inputReader().readText()
-
-        fun executeAndReadLines(cmd: String, vararg args: String): List<String> =
-            Runtime.getRuntime().exec(
-                mutableListOf<String>().apply {
-                    add(cmd)
-                    addAll(args)
-                }.toTypedArray()
-            ).apply {
-                waitFor()
-            }.inputReader().readText().lines()
-    }
-
-    enum class ShellType(
-        val cmd: String
-    ) {
-        SH("/bin/sh"),
-        BASH("/bin/bash")
-    }
+    fun executeAndRead(
+        cmd: String,
+        vararg args: String
+    ): String =
+        Runtime.getRuntime().exec(
+            mutableListOf<String>().apply {
+                add(cmd)
+                addAll(args)
+            }.toTypedArray()
+        ).apply {
+            waitFor()
+        }.inputReader().readText()
+
+    fun executeAndReadLines(cmd: String, vararg args: String): List<String> =
+        Runtime.getRuntime().exec(
+            mutableListOf<String>().apply {
+                add(cmd)
+                addAll(args)
+            }.toTypedArray()
+        ).apply {
+            waitFor()
+        }.inputReader().readText().lines()
 }

+ 2 - 4
src/main/kotlin/eu/mjdev/desktop/icons/Icons.kt

@@ -3,11 +3,8 @@ package eu.mjdev.desktop.icons
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.automirrored.filled.Chat
 import androidx.compose.material.icons.automirrored.filled.Send
-import androidx.compose.material.icons.filled.ContentCopy
-import androidx.compose.material.icons.filled.Visibility
-import androidx.compose.material.icons.filled.VisibilityOff
 import androidx.compose.material.icons.automirrored.filled.TextSnippet
-import androidx.compose.material.icons.filled.Campaign
+import androidx.compose.material.icons.filled.*
 
 @Suppress("unused")
 object Icons {
@@ -18,4 +15,5 @@ object Icons {
     val TextSnippet = Icons.AutoMirrored.Filled.TextSnippet
     val Campaign = Icons.Filled.Campaign
     val Chat = Icons.AutoMirrored.Filled.Chat
+    val User = Icons.Filled.VerifiedUser
 }

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/helpers/managers/ConnectivityManager.kt → src/main/kotlin/eu/mjdev/desktop/managers/ConnectivityManager.kt

@@ -1,4 +1,4 @@
-package eu.mjdev.desktop.helpers.managers
+package eu.mjdev.desktop.managers
 
 import dev.tmapps.konnection.Konnection
 import dev.tmapps.konnection.NetworkConnection

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/helpers/managers/FileSystemWatcher.kt → src/main/kotlin/eu/mjdev/desktop/managers/FileSystemWatcher.kt

@@ -1,4 +1,4 @@
-package eu.mjdev.desktop.helpers.managers
+package eu.mjdev.desktop.managers
 
 import io.github.irgaly.kfswatch.KfsDirectoryWatcher
 import io.github.irgaly.kfswatch.KfsDirectoryWatcherEvent

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/helpers/managers/GnomeManager.kt → src/main/kotlin/eu/mjdev/desktop/managers/GnomeManager.kt

@@ -1,4 +1,4 @@
-package eu.mjdev.desktop.helpers.managers
+package eu.mjdev.desktop.managers
 
 import eu.mjdev.desktop.helpers.system.Shell
 

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/helpers/managers/KCEFHelper.kt → src/main/kotlin/eu/mjdev/desktop/managers/KCEFHelper.kt

@@ -1,4 +1,4 @@
-package eu.mjdev.desktop.helpers.managers
+package eu.mjdev.desktop.managers
 
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf

+ 546 - 0
src/main/kotlin/eu/mjdev/desktop/managers/WindowsManager.kt

@@ -0,0 +1,546 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+@file:Suppress("LocalVariableName")
+
+package eu.mjdev.desktop.managers
+
+import com.sun.jna.platform.unix.X11.*
+import eu.mjdev.desktop.data.App
+import eu.mjdev.desktop.provider.DesktopProvider
+import kotlinx.coroutines.*
+
+@Suppress("unused")
+class WindowsManager(
+    val api: DesktopProvider,
+    val scope: CoroutineScope = api.scope,
+) {
+//    private var loop: Job? = null
+//    private val x11: X11 by lazy { Native.load("X11", X11::class.java) }
+//    private val x11Ext by lazy { Native.load("X11", X11Ext::class.java) }
+//    private val display: Display by lazy { x11.XOpenDisplay(null) }
+
+//    private val rootWindow: Window
+//        get() = x11.XDefaultRootWindow(display)
+
+//    val activeWindow
+//        get() = get_active_window()
+
+//    val windows = getAllWindows(rootWindow, 0)
+
+//    val environment: Environment = Environment()
+//    val currentDisplay
+//        get() = environment["WAYLAND_DISPLAY"]
+//    val WlRegistryEvents = object : WlRegistryEvents {
+//        override fun global(emitter: WlRegistryProxy?, name: Int, interface_: String, version: Int) {
+//            println("Got global event: $name")
+//        }
+//
+//        override fun globalRemove(emitter: WlRegistryProxy?, name: Int) {
+//            println("Got global remove event: $name")
+//        }
+//    }
+//    val display: WlDisplayProxy by lazy { WlDisplayProxy.connect(currentDisplay) }
+//    val registry: WlRegistryProxy by lazy { display.getRegistry(WlRegistryEvents) }
+
+    // todo
+    val windows
+        get() = findWindows()
+
+    // todo
+    private val activeWindow: SystemWindow?
+        get() = null
+
+    // todo
+    private fun findWindows(app: App? = null): List<SystemWindow> = listOf<SystemWindow>().let { list ->
+        if (app == null) list else list.filter { w -> w.windowClass == app.windowClass }
+    }
+
+//    init {
+//        println("Current display : $currentDisplay")
+//        println("Registry : ${registry.id}")
+//    }
+
+    fun dispose() {
+//        loop?.cancel()
+//        x11.XCloseDisplay(display)
+    }
+
+//    fun findWindows(app: App): List<SystemWindow> = windows.filter { w ->
+//        println("w: (${w.id},${w.windowClass}, ${w.command})")
+//        w.windowClass.isNotEmpty()
+//    }.filter { w ->
+//        w.windowClass.trim().contains(app.windowClass, true) ||
+//                w.command.trim().contains(app.windowClass, true) ||
+//                w.command.trim().contains(app.fullAppName, true) ||
+//                w.command.trim().contentEquals(app.cmd, true)
+//    }.apply {
+//        println("app[${app.windowClass}] : ${count()}")
+//    }
+
+    fun hasWindow(app: App): Boolean =
+        findWindows(app).isNotEmpty()
+
+    fun isWindowFocus(app: App): Boolean {
+        val appWindow = windows.firstOrNull { it.windowClass.contentEquals(app.windowClass, true) }
+        return (appWindow != null && activeWindow != null && appWindow.id == activeWindow?.id)
+    }
+
+    @Suppress("UNUSED_PARAMETER", "CanBeVal", "KotlinConstantConditions")
+    fun minimizeWindows(app: App): Boolean {
+        var isMinimized = false
+//        findWindows(app).forEach { sw ->
+//            isMinimized = isMinimized or minimizeWindow(sw.window)
+//        }
+        return isMinimized
+    }
+
+    @Suppress("UNUSED_PARAMETER", "CanBeVal", "KotlinConstantConditions")
+    fun requestWindowsFocus(app: App): Boolean {
+        var isRequested = false
+//        findWindows(app).forEach { sw ->
+//            isRequested = isRequested or activateWindow(sw.window)
+//        }
+        return isRequested
+    }
+
+    @Suppress("unused", "UNUSED_PARAMETER", "KotlinConstantConditions", "CanBeVal")
+    fun closeWindows(app: App): Boolean {
+        var isClosed = false
+//        findWindows(app).forEach { sw ->
+//            isClosed = isClosed or closeWindow(sw.window)
+//        }
+        return isClosed
+    }
+
+//    @Suppress("DEPRECATED_IDENTITY_EQUALS")
+//    private fun getAllWindows(root: Window, depth: Int): List<SystemWindow> {
+//        val windowList = mutableListOf<SystemWindow>()
+//        val windowRef = WindowByReference()
+//        val parentRef = WindowByReference()
+//        val childrenRef = PointerByReference()
+//        val childCountRef = IntByReference()
+//        x11.XQueryTree(display, root, windowRef, parentRef, childrenRef, childCountRef)
+//        if (childrenRef.value == null) return emptyList()
+//        val ids: LongArray
+//        if (Native.LONG_SIZE === java.lang.Long.BYTES) {
+//            ids = childrenRef.value.getLongArray(0, childCountRef.value)
+//        } else if (Native.LONG_SIZE === Integer.BYTES) {
+//            val intIds: IntArray = childrenRef.value.getIntArray(0, childCountRef.value)
+//            ids = LongArray(intIds.size)
+//            for (i in intIds.indices) {
+//                ids[i] = intIds[i].toLong()
+//            }
+//        } else return emptyList()
+//        for (id in ids) {
+//            if (id == 0L) continue
+//            Window(id).also { window ->
+//                val pid = getWindowPid(window) ?: 0L
+////                val name = getWindowName(window) ?: ""
+//                val cls = getWindowClass(window) ?: ""
+//                val desk = getWindowDesktop(window) ?: 0L
+//                val process = ProcessHandle.allProcesses().toList().firstOrNull { it.pid() == pid }
+////                val icon = getWindowIconName(window) ?: ""
+//                windowList.add(
+//                    SystemWindow(
+//                        id,
+////                        name,
+////                        icon,
+//                        pid,
+//                        cls,
+//                        desk,
+//                        process?.info()?.command()?.get().orEmpty(),
+//                    )
+//                )
+//                getAllWindows(window, depth + 1)
+//            }
+//        }
+//        return windowList
+//    }
+
+//    private fun getWindowPid(
+//        window: Window
+//    ) = get_property_as_long(
+//        window,
+//        XA_CARDINAL,
+//        "_NET_WM_PID"
+//    )
+
+//    @Suppress("unused")
+//    private fun getWindowName(
+//        window: Window
+//    ) = get_property_as_utf8_string(
+//        window,
+//        x11.XInternAtom(display, "UTF8_STRING", false),
+//        "_NET_WM_NAME"
+//    )
+
+//    private fun getWindowClass(
+//        window: Window
+//    ) = get_property_as_utf8_string(
+//        window,
+//        x11.XInternAtom(display, "UTF8_STRING", false),
+//        "WM_CLASS"
+//    ) ?: get_property_as_string(
+//        window,
+//        XA_STRING,
+//        "WM_CLASS"
+//    )
+
+//    @Suppress("unused")
+//    private fun getWindowCommand(
+//        pid: Long
+//    ): String = ProcessHandle.allProcesses().toList().firstOrNull {
+//        it.pid() == pid
+//    }?.info()?.command()?.getOrNull().orEmpty()
+
+//    private fun getWindowDesktop(
+//        window: Window
+//    ) = get_property_as_long(
+//        window,
+//        XA_CARDINAL,
+//        "_NET_SHOWING_DESKTOP"
+//    )
+
+//    @Suppress("unused")
+//    private fun getWindowIconName(
+//        window: Window
+//    ): String? = PointerByReference().let { icon_name_return ->
+//        if (x11Ext.XGetIconName(display, window, icon_name_return) == 0) {
+//            g_free(icon_name_return.pointer)
+//            null
+//        } else {
+//            g_strdup(icon_name_return.pointer)
+//        }
+//    }
+
+//    private fun minimizeWindow(
+//        window: Window
+//    ): Boolean = x11Ext.XIconifyWindow(display, window, x11.XDefaultScreen(display)) == TRUE
+
+//    private fun activateWindow(
+//        window: Window,
+//        switchDesktop: Boolean = true
+//    ) = activate_window(window, switchDesktop)
+
+//    private fun closeWindow(
+//        window: Window
+//    ) = client_msg(window, "_NET_CLOSE_WINDOW", 0, 0, 0, 0, 0)
+
+//    @Suppress("unused")
+//    private fun g_free(
+//        pointer: Pointer?
+//    ) {
+//        if (pointer != null) x11.XFree(pointer)
+//    }
+
+//    @Suppress("SameParameterValue")
+//    private fun get_property_as_long(
+//        window: Window,
+//        xa_prop_type: Atom,
+//        prop_name: String
+//    ): Long? = get_property_as_long(window, xa_prop_type, prop_name, null)
+
+//    @Suppress("SameParameterValue")
+//    private fun get_property_as_long(
+//        window: Window,
+//        xa_prop_type: Atom,
+//        prop_name: String,
+//        size: NativeLongByReference? = null
+//    ): Long? {
+//        var longProp: Long? = null
+//        val prop = get_property(window, xa_prop_type, prop_name, size)
+//        if (prop != null) {
+//            longProp = prop.getLong(0)
+////            g_free(prop)
+//        }
+//        return longProp
+//    }
+
+//    @Suppress("SameParameterValue")
+//    private fun get_property_as_int(
+//        win: Window,
+//        xa_prop_type: Atom,
+//        prop_name: String
+//    ): Int? {
+//        var intProp: Int? = null
+//        val prop = get_property(win, xa_prop_type, prop_name, null)
+//        if (prop != null) {
+//            intProp = prop.getInt(0)
+////            g_free(prop)
+//        }
+//        return intProp
+//    }
+
+//    @Suppress("SameParameterValue")
+//    private fun get_property_as_string(
+//        win: Window,
+//        xa_prop_type: Atom,
+//        prop_name: String
+//    ): String? {
+//        var strProp: String? = null
+//        val prop = get_property(
+//            win, xa_prop_type, prop_name,
+//            null
+//        )
+//        if (prop != null) {
+//            strProp = g_strdup(prop)
+////            g_free(prop)
+//        }
+//        return strProp
+//    }
+
+//    private fun g_strdup(
+//        pointer: Pointer
+//    ): String {
+//        val value = pointer.getString(0)
+//        // g_free(pointer);
+//        return value
+//    }
+
+//    private fun g_locale_to_utf8(
+//        pointer: Pointer
+//    ): String = g_strdup(pointer)
+
+//    @Suppress("SameParameterValue")
+//    private fun get_property_as_utf8_string(
+//        window: Window,
+//        xa_prop_type: Atom,
+//        prop_name: String
+//    ): String? {
+//        var strProp: String? = null
+//        val prop = get_property(window, xa_prop_type, prop_name, null)
+//        if (prop != null) {
+//            strProp = g_locale_to_utf8(prop)
+////            g_free(prop)
+//        }
+//        return strProp
+//    }
+
+//    @Suppress("SameParameterValue")
+//    private fun get_property_as_window(
+//        window: Window,
+//        xa_prop_type: Atom,
+//        prop_name: String
+//    ): Window? {
+//        var ret: Window? = null
+//        val prop = get_property(window, xa_prop_type, prop_name, null)
+//        if (prop != null) {
+//            ret = Window(prop.getLong(0))
+////            g_free(prop)
+//        }
+//        return ret
+//    }
+
+//    private fun get_property(
+//        window: Window?,
+//        xa_prop_type: Atom,
+//        prop_name: String?,
+//        size: NativeLongByReference?
+//    ): Pointer? {
+//        val xa_ret_type = AtomByReference()
+//        val ret_format = IntByReference()
+//        val ret_n_items = NativeLongByReference()
+//        val ret_bytes_after = NativeLongByReference()
+//        val ret_prop = PointerByReference()
+//        val xa_prop_name = x11.XInternAtom(display, prop_name, false)
+//        if (x11.XGetWindowProperty(
+//                display,
+//                window,
+//                xa_prop_name,
+//                NativeLong(0),
+//                NativeLong((MAX_PROPERTY_VALUE_LEN / 4).toLong()),
+//                false,
+//                xa_prop_type,
+//                xa_ret_type,
+//                ret_format,
+//                ret_n_items,
+//                ret_bytes_after,
+//                ret_prop
+//            ) != Success
+//        ) {
+//            return null
+//        }
+//        if ((xa_ret_type.value == null) || (xa_ret_type.value.toLong() != xa_prop_type.toLong())
+//        ) {
+////            g_free(ret_prop.pointer)
+//            return null
+//        }
+//        if (size != null) {
+//            var tmp_size = ((ret_format.value / 8) * ret_n_items.value.toLong())
+//            if (ret_format.value == 32) {
+//                tmp_size *= (NativeLong.SIZE / 4).toLong()
+//            }
+//            size.value = NativeLong(tmp_size)
+//        }
+//        return ret_prop.value
+//    }
+
+//    @Suppress("SameParameterValue")
+//    private fun client_msg(
+//        window: Window,
+//        msg: String,
+//        data0: Long,
+//        data1: Long,
+//        data2: Long,
+//        data3: Long,
+//        data4: Long
+//    ): Boolean {
+//        val mask = NativeLong((SubstructureRedirectMask or SubstructureNotifyMask).toLong())
+//        val xclient = XClientMessageEvent()
+//        xclient.type = ClientMessage
+//        xclient.serial = NativeLong(0)
+//        xclient.send_event = TRUE
+//        xclient.message_type = x11.XInternAtom(display, msg, false)
+//        xclient.window = window
+//        xclient.format = 32
+//        xclient.data.setType(Array<NativeLong>::class.java)
+//        xclient.data.l[0] = NativeLong(data0)
+//        xclient.data.l[1] = NativeLong(data1)
+//        xclient.data.l[2] = NativeLong(data2)
+//        xclient.data.l[3] = NativeLong(data3)
+//        xclient.data.l[4] = NativeLong(data4)
+//        val event = XEvent()
+//        event.setTypedValue(xclient)
+//        return if (x11.XSendEvent(
+//                display,
+//                x11.XDefaultRootWindow(display),
+//                FALSE,
+//                mask,
+//                event
+//            ) != FALSE
+//        ) EXIT_SUCCESS else EXIT_FAILURE
+//    }
+
+//    private fun activate_window(
+//        window: Window,
+//        switch_desktop: Boolean = true
+//    ): Boolean {
+//        var desktop: Int? = get_property_as_int(window, XA_CARDINAL, "_NET_WM_DESKTOP")
+//        if (desktop == null) {
+//            desktop = get_property_as_int(window, XA_CARDINAL, "_WIN_WORKSPACE")
+//        }
+//        if (switch_desktop && (desktop != null)) {
+//            client_msg(
+//                x11.XDefaultRootWindow(display),
+//                "_NET_CURRENT_DESKTOP",
+//                desktop.toLong(),
+//                0,
+//                0,
+//                0,
+//                0
+//            )
+//        }
+//        client_msg(window, "_NET_ACTIVE_WINDOW", 0, 0, 0, 0, 0)
+//        x11.XMapRaised(display, window)
+//        return EXIT_SUCCESS
+//    }
+
+//    private fun get_active_window(): Window? = get_property_as_window(
+//        x11.XDefaultRootWindow(display),
+//        XA_WINDOW,
+//        "_NET_ACTIVE_WINDOW"
+//    )
+
+//    companion object {
+//        const val MAX_PROPERTY_VALUE_LEN = 4096
+//        const val TRUE: Int = 1
+//        const val FALSE: Int = 0
+//        const val EXIT_SUCCESS: Boolean = true
+//        const val EXIT_FAILURE: Boolean = false
+//    }
+
+    data class SystemWindow(
+        val id: Long,
+        val pid: Long,
+        val windowClass: String,
+        val desktop: Long,
+//        val name: String,
+//        val iconName: String,
+        val command: String,
+    ) {
+        companion object {
+            val SystemWindow.window: Window get() = Window(id)
+        }
+    }
+
+//    private interface Xmu : Library {
+//        fun XmuClientWindow(display: Display?, window: Window?): Window?
+//    }
+
+//    private interface X11Ext : Library {
+//        fun XMoveWindow(
+//            display: Display?,
+//            window: Window?,
+//            x: Long,
+//            y: Long
+//        )
+
+//        fun XResizeWindow(
+//            display: Display?,
+//            window: Window?,
+//            width: Long,
+//            height: Long
+//        )
+
+//        fun XMoveResizeWindow(
+//            display: Display?,
+//            window: Window?,
+//            x: Long,
+//            y: Long,
+//            width: Long,
+//            height: Long
+//        )
+
+//        fun XCreateFontCursor(
+//            display: Display?,
+//            shape: Long
+//        ): Cursor?
+
+//        fun XGrabPointer(
+//            display: Display?,
+//            grab_window: Window?,
+//            owner_events: Int,
+//            event_mask: NativeLong?,
+//            pointer_mode: Int,
+//            keyboard_mode: Int,
+//            confine_to: Window?,
+//            cursor: Cursor?,
+//            time: Int
+//        ): Int
+
+//        fun XAllowEvents(
+//            display: Display?,
+//            event_mode: Int,
+//            time: Int
+//        ): Int
+
+//        fun XUngrabPointer(
+//            display: Display?,
+//            time: Int
+//        ): Int
+
+//        fun XmuClientWindow(
+//            display: Display?,
+//            window: Window?
+//        ): Window?
+
+//        fun XGetIconName(
+//            display: Display?,
+//            window: Window?,
+//            icon_name_return: PointerByReference?
+//        ): Int
+
+//        fun XIconifyWindow(
+//            display: Display?,
+//            window: Window?,
+//            screen: Int
+//        ): Int
+
+//    }
+}

+ 31 - 44
src/main/kotlin/eu/mjdev/desktop/provider/AppsProvider.kt

@@ -7,7 +7,7 @@ import eu.mjdev.desktop.data.DesktopFile
 import eu.mjdev.desktop.extensions.Custom.invalidate
 import eu.mjdev.desktop.extensions.Custom.jsonToList
 import eu.mjdev.desktop.extensions.Locale.toLocale
-import eu.mjdev.desktop.helpers.exception.EmptyException.Companion.EmptyException
+import eu.mjdev.desktop.helpers.exception.SuccessException
 import eu.mjdev.desktop.helpers.system.Shell
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
@@ -139,40 +139,27 @@ class AppsProvider(
 
     val favoriteApps = mutableStateListOf<App>()
 
-    fun startApp(
-        app: App,
-        windowsManager: WindowsManager = api.windowsManager
-    ) {
-        scope.launch(Dispatchers.IO) {
-            if (app.isRunning) {
-                // todo menu to switch
-                if (windowsManager.isWindowFocus(app)) {
-                    windowsManager.minimizeWindow(app)
-                } else {
-                    windowsManager.requestWindowFocus(app)
-                }
+    fun startApp(app: App) = scope.launch(Dispatchers.IO) {
+        app.onStarting {
+            if (favoriteApps.contains(app)) {
+                favoriteApps.invalidate()
             } else {
-                app.onStarting {
-                    if (favoriteApps.contains(app)) {
-                        favoriteApps.invalidate()
-                    } else {
-                        favoriteApps.add(app)
-                    }
-                }.onStarted {
-                    favoriteApps.invalidate()
-                }.onStop { result ->
-                    if (!app.isFavorite) {
-                        favoriteApps.remove(app)
-                    } else {
-                        favoriteApps.invalidate()
-                    }
-                    if (result != EmptyException) {
-                        result.printStackTrace()
-                    }
-                }.start(api)
+                favoriteApps.add(app)
             }
+        }.onStarted {
             favoriteApps.invalidate()
-        }
+        }.onStop { result ->
+            if (app.isFavorite) {
+                favoriteApps.invalidate()
+            } else {
+                favoriteApps.remove(app)
+            }
+            when (result) {
+                is SuccessException -> println(result.message)
+                else -> result.printStackTrace()
+            }
+        }.start()
+        favoriteApps.invalidate()
     }
 
     init {
@@ -182,18 +169,18 @@ class AppsProvider(
             "org.gnome.shell",
             "favorite-apps"
         ).jsonToList<String>().mapNotNull { deskFileName ->
-                allAppsDesktopFiles.filter { deskFile ->
-                    deskFile.file.name?.contentEquals(deskFileName) == true
-                }.map { deskFile ->
-                    App(
-                        desktopFile = deskFile,
-                        file = deskFile.file,
-                        isFavorite = true
-                    )
-                }.firstOrNull()
-            }?.also { favorite ->
-                favoriteApps.addAll(favorite)
-            }
+            allAppsDesktopFiles.filter { deskFile ->
+                deskFile.file.name?.contentEquals(deskFileName) == true
+            }.map { deskFile ->
+                App(
+                    desktopFile = deskFile,
+                    file = deskFile.file,
+                    isFavorite = true
+                )
+            }.firstOrNull()
+        }?.also { favorite ->
+            favoriteApps.addAll(favorite)
+        }
     }
 
 }

+ 13 - 13
src/main/kotlin/eu/mjdev/desktop/provider/DesktopProvider.kt

@@ -16,10 +16,11 @@ import eu.mjdev.desktop.extensions.Compose.asyncImageLoader
 import eu.mjdev.desktop.helpers.adb.AdbDiscover.Companion.adbDevicesHandler
 import eu.mjdev.desktop.helpers.bitmap.Bitmap
 import eu.mjdev.desktop.helpers.internal.Palette
-import eu.mjdev.desktop.helpers.managers.*
+import eu.mjdev.desktop.managers.*
 import eu.mjdev.desktop.helpers.system.DBus
 import eu.mjdev.desktop.helpers.system.OsRelease
 import eu.mjdev.desktop.helpers.system.Shell
+import eu.mjdev.desktop.managers.WindowsManager
 import eu.mjdev.desktop.provider.AIProvider.AiPluginGemini
 import eu.mjdev.desktop.theme.gtk.GtkThemeHelper
 import kotlinx.coroutines.CoroutineScope
@@ -35,10 +36,11 @@ import kotlin.system.exitProcess
 @Suppress("unused", "MemberVisibilityCanBePrivate", "SameParameterValue")
 class DesktopProvider(
     val scope: CoroutineScope = CoroutineScope(Dispatchers.IO),
+    val args: List<String> = emptyList(),
     val imageLoader: ImageLoader? = null,
     val isDebug: Boolean = true,
     val isFirstStart: Boolean = true, // todo
-    val isInstalled:Boolean = false, // todo
+    val isInstalled: Boolean = false, // todo
 ) : AutoCloseable {
     val scriptManager: ScriptEngineManager by lazy { ScriptEngineManager() }
     val scriptEngine: ScriptEngine by lazy {
@@ -96,11 +98,10 @@ class DesktopProvider(
         get() = runCatching {
             toolkit.isAlwaysOnTopSupported
         }.getOrNull() ?: false
-
-    //    val cursorSize
-//        get() = toolkit.getBestCursorSize(32, 32).let {
-//            DpSize(it.width.dp, it.height.dp)
-//        }
+    val cursorSize
+        get() = toolkit.getBestCursorSize(32, 32).let {
+            DpSize(it.width.dp, it.height.dp)
+        }
     val desktopUtils: Desktop by lazy {
         Desktop.getDesktop()
     }
@@ -114,9 +115,6 @@ class DesktopProvider(
         get() = graphicsDevice.isWindowTranslucencySupported(GraphicsDevice.WindowTranslucency.TRANSLUCENT)
 
     init {
-        runCatching {
-            windowsManager.init()
-        }
         runCatching {
             currentUser.theme.controlCenterExpandedWidth = containerSize.width.div(4)
         }
@@ -255,12 +253,14 @@ class DesktopProvider(
             scope: CoroutineScope = rememberCoroutineScope(),
             imageLoader: ImageLoader = asyncImageLoader(),
             isDebug: Boolean = LocalInspectionMode.current,
+            args: List<String> = emptyList(),
         ) = remember {
             println("default initialized desktop provider created")
             DesktopProvider(
-                scope,
-                imageLoader,
-                isDebug,
+                scope = scope,
+                imageLoader = imageLoader,
+                args = args,
+                isDebug = isDebug || args.contains("-d")
             )
         }
 

+ 0 - 70
src/main/kotlin/eu/mjdev/desktop/provider/WindowsManager.kt

@@ -1,70 +0,0 @@
-package eu.mjdev.desktop.provider
-
-import androidx.compose.runtime.mutableStateListOf
-import eu.mjdev.desktop.data.App
-import eu.mjdev.desktop.data.DesktopWindow
-import java.awt.Desktop
-import java.awt.desktop.*
-
-// todo
-@Suppress("UNUSED_PARAMETER", "unused")
-class WindowsManager(
-    val api: DesktopProvider,
-    val desktop: Desktop = api.desktopUtils
-) : AppForegroundListener, AppHiddenListener, AppReopenedListener {
-    private val windows = mutableStateListOf<DesktopWindow>()
-
-    fun init() {
-        desktop.addAppEventListener(this)
-    }
-
-    fun dispose() {
-        desktop.removeAppEventListener(this)
-    }
-
-    override fun appRaisedToForeground(
-        e: AppForegroundEvent?
-    ) {
-        println(e)
-    }
-
-    override fun appMovedToBackground(
-        e: AppForegroundEvent?
-    ) {
-        println(e)
-    }
-
-    override fun appHidden(
-        e: AppHiddenEvent?
-    ) {
-        println(e)
-    }
-
-    override fun appUnhidden(
-        e: AppHiddenEvent?
-    ) {
-        println(e)
-    }
-
-    override fun appReopened(
-        e: AppReopenedEvent?
-    ) {
-        println(e)
-    }
-
-    fun isWindowFocus(
-        app: App
-    ) = false
-
-    fun minimizeWindow(
-        app: App
-    ) = false
-
-    fun maximizeWindow(
-        app: App
-    ) = false
-
-    fun requestWindowFocus(
-        app: App
-    ) = false
-}

+ 2 - 2
src/main/kotlin/eu/mjdev/desktop/theme/gtk/GtkThemeHelper.kt

@@ -6,8 +6,8 @@ import eu.mjdev.desktop.data.DesktopFile.Companion.desktopFile
 import eu.mjdev.desktop.extensions.Compose.SuperDarkGray
 import eu.mjdev.desktop.extensions.Compose.rgbToHex
 import eu.mjdev.desktop.helpers.internal.Palette
-import eu.mjdev.desktop.helpers.managers.GnomeManager.Companion.THEME_ADWAITA_DARK
-import eu.mjdev.desktop.helpers.managers.GnomeManager.Companion.THEME_MJDEV
+import eu.mjdev.desktop.managers.GnomeManager.Companion.THEME_ADWAITA_DARK
+import eu.mjdev.desktop.managers.GnomeManager.Companion.THEME_MJDEV
 import eu.mjdev.desktop.provider.DesktopProvider
 import java.io.File
 

+ 3 - 2
src/main/kotlin/eu/mjdev/desktop/windows/Window.kt

@@ -13,6 +13,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.unit.*
 import androidx.compose.ui.window.*
 import eu.mjdev.desktop.windows.ChromeWindowState.Companion.rememberChromeWindowState
+import eu.mjdev.desktop.windows.blur.WindowBlurManager.Companion.windowBlur
 import java.awt.*
 import java.awt.event.*
 import java.util.*
@@ -47,7 +48,7 @@ fun Window(
     alwaysOnTop: Boolean = false,
     alwaysOnBottom: Boolean = false,
     closeAction: Int = JFrame.DO_NOTHING_ON_CLOSE,
-//    blurEnabled: Boolean = true,
+    blurEnabled: Boolean = true,
     focusHelper: WindowFocusHelper,
     stateHelper: WindowStateHelper,
     onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
@@ -183,7 +184,7 @@ fun Window(
             }
         },
         content = {
-//            windowBlur(blurEnabled)
+            windowBlur(blurEnabled)
             stateHelper.register(window)
             focusHelper.register(window)
             content()

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/windows/WindowStateHelper.kt

@@ -84,6 +84,6 @@ class WindowStateHelper(
 
     fun requestFocus() {
         window?.toFront()
-//        window?.requestFocus()
+        window?.requestFocus()
     }
 }

+ 1 - 0
src/main/kotlin/eu/mjdev/desktop/windows/blur/base/HackedContentPane.kt

@@ -14,6 +14,7 @@ class HackedContentPane : JPanel() {
 
     init {
         isOpaque = false
+//        setLayout(BorderLayout())
 //        add(jlayer)
     }
 

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/windows/blur/linux/LinuxWindowBlurManager.kt

@@ -102,7 +102,7 @@ class LinuxWindowBlurManager(
         }
 
         fun Window.hackContentPane() {
-            println("hacking window: ${this.name}")
+//            println("hacking window: ${this.name}")
             val oldContentPane = contentPane ?: return
             val newContentPane: JComponent = HackedContentPane()
             newContentPane.name = "$name.contentPane"