浏览代码

release fix

Milan Jurkulak 3 月之前
父节点
当前提交
0f303c9024

+ 213 - 62
.github/workflows/release.yml

@@ -1,12 +1,12 @@
 name: Release
 
 # Produces versioned distributables for ALL targets a host can build, into a GitHub Release.
-# Each format is built independently so one failing format never blocks the others, and the
-# collect step uploads whatever actually got produced (partial success is still published).
-#   linux (deb/rpm/AppImage) + android (apk) + iso  -> ubuntu-latest
+# Each target verifies its own required artifacts. Publishing uses every successful target, so
+# apk/exe are not held hostage by Linux-only packaging dependencies.
+#   linux (deb/rpm/AppImage) + iso                  -> debian:trixie
+#   android (apk)                                   -> ubuntu-latest
 #   windows (exe)                                   -> windows-latest
 #   macos   (dmg)                                   -> macos-latest
-# Skipped/failed formats are logged as ::warning:: and never fail the whole release.
 # NOTE: iOS cannot be produced — the project has no iOS Kotlin target (commonMain uses
 #       JVM-only libs); there is no iOS step.
 
@@ -24,6 +24,152 @@ permissions:
   contents: read
 
 jobs:
+  package-linux-iso:
+    name: Package (linux-iso)
+    runs-on: ubuntu-latest
+    container:
+      image: debian:trixie
+      options: --privileged
+    steps:
+      - name: Install Debian build dependencies
+        shell: bash
+        run: |
+          set -euo pipefail
+          apt-get update
+          apt-get install -y --no-install-recommends \
+            bash ca-certificates coreutils curl findutils git \
+            tar unzip xz-utils zip zstd \
+            build-essential pkg-config rpm fakeroot \
+            debootstrap squashfs-tools xorriso mtools dpkg-dev \
+            grub-common grub-pc-bin grub-efi-amd64-bin debian-archive-keyring \
+            wayland-protocols libwayland-dev libwayland-bin \
+            libxkbcommon-dev libcairo2-dev libcairo2 libpixman-1-dev libdrm-dev \
+            libseat-dev libgbm-dev libegl-dev libgles-dev \
+            libcap-dev liblcms2-dev libsystemd-dev libudev-dev libvulkan-dev \
+            libxcb1-dev libxcb-composite0-dev libxcb-dri3-dev \
+            libxcb-image0-dev libxcb-present-dev libxcb-render0-dev \
+            libxcb-render-util0-dev libxcb-res0-dev libxcb-shm0-dev libxcb-xfixes0-dev \
+            libxcb-ewmh-dev libxcb-icccm4-dev libxcb-xinput-dev libxcb-errors-dev \
+            libx11-xcb-dev libwlroots-0.18-dev
+
+          wayland-scanner --version
+          pkg-config --modversion wlroots-0.18
+
+      - uses: actions/checkout@v4
+
+      - name: Set up JDK 17
+        uses: actions/setup-java@v4
+        with:
+          java-version: '17'
+          distribution: 'temurin'
+
+      - name: Setup Gradle
+        uses: gradle/actions/setup-gradle@v4
+        with:
+          cache-read-only: false
+          gradle-home-cache-cleanup: true
+
+      - name: Cache Kotlin/Native
+        uses: actions/cache@v4
+        with:
+          path: ~/.konan
+          key: ${{ runner.os }}-debian-trixie-konan-${{ hashFiles('gradle/libs.versions.toml', 'gradle.properties', 'settings.gradle.kts', 'build.gradle.kts', 'compositor/*.gradle.kts') }}
+          restore-keys: |
+            ${{ runner.os }}-debian-trixie-konan-
+
+      - name: Cache AppImage tool
+        uses: actions/cache@v4
+        with:
+          path: .gradle/tools
+          key: ${{ runner.os }}-debian-trixie-appimagetool-${{ hashFiles('gradle/libs.versions.toml', 'build.gradle.kts') }}
+          restore-keys: |
+            ${{ runner.os }}-debian-trixie-appimagetool-
+
+      - name: Make gradlew executable
+        shell: bash
+        run: chmod +x gradlew || true
+
+      - name: Read app name + version
+        id: meta
+        shell: bash
+        run: |
+          echo "version=$(grep -E '^app-pkg-version' gradle/libs.versions.toml | sed -E 's/.*"(.*)".*/\1/')" >> "$GITHUB_OUTPUT"
+          echo "appname=$(grep -E '^app-name' gradle/libs.versions.toml | sed -E 's/.*"(.*)".*/\1/')" >> "$GITHUB_OUTPUT"
+
+      # One Gradle invocation keeps dependency resolution/configuration cache hot and avoids
+      # re-entering packageFullDeb before makeIso. --continue keeps independent artifacts building
+      # when one Linux format fails; artifact collection below publishes whatever succeeded.
+      - name: Build linux artifacts
+        shell: bash
+        continue-on-error: true
+        run: ./gradlew packageAppImageFile :desktopApp:packageReleaseRpm packageFullDeb makeIso -PdepCheck=false --stacktrace --continue
+
+      # Primary source is releases/ (collectReleases + makeIso). Fall back to jpackage output
+      # dirs for partial builds.
+      - name: Collect artifacts
+        if: always()
+        shell: bash
+        run: |
+          set -euo pipefail
+          VER='${{ steps.meta.outputs.version }}'
+          APP='${{ steps.meta.outputs.appname }}'
+          BASE="$APP-$VER"
+          PKG=packages/main-release
+          mkdir -p upload
+
+          copy_pkg() {
+            local dir="$1" ext="$2"
+            for f in "$dir"/*."$ext"; do
+              if [ -e "$f" ]; then
+                cp -v "$f" "upload/$BASE.$ext"
+                return 0
+              fi
+            done
+            return 0
+          }
+
+          if compgen -G "releases/*" > /dev/null; then
+            cp -av releases/* upload/
+          fi
+
+          [ -f "upload/$BASE.deb" ] || copy_pkg "$PKG/deb" deb
+          [ -f "upload/$BASE.rpm" ] || copy_pkg "$PKG/rpm" rpm
+          [ -f "upload/$BASE.AppImage" ] || copy_pkg "$PKG/appimage" AppImage
+          if [ ! -f "upload/$BASE.iso" ]; then
+            for f in releases/*.iso; do
+              [ -e "$f" ] && cp -v "$f" "upload/$BASE.iso" && break
+            done
+          fi
+
+          if [ -d reports ] && [ -n "$(ls -A reports 2>/dev/null)" ]; then
+            (cd reports && zip -r "../upload/$BASE-reports.zip" .)
+          fi
+          echo "=== produced for linux-iso ==="
+          ls -la upload/
+
+          require_artifact() {
+            local file="upload/$1"
+            if [ ! -s "$file" ]; then
+              echo "::error::required release artifact is missing or empty: $1"
+              exit 1
+            fi
+          }
+
+          for required in "$BASE.deb" "$BASE.rpm" "$BASE.AppImage" "$BASE.iso"; do
+            if [ ! -s "upload/$required" ]; then
+              echo "::warning::linux artifact was not produced: $required"
+            fi
+          done
+          require_artifact "$BASE.iso"
+
+      - name: Upload build artifacts
+        if: always()
+        uses: actions/upload-artifact@v4
+        with:
+          name: dist-linux-iso
+          path: upload/
+          if-no-files-found: warn
+
   package:
     name: Package (${{ matrix.target }})
     strategy:
@@ -31,7 +177,7 @@ jobs:
       matrix:
         include:
           - os: ubuntu-latest
-            target: linux-android-iso
+            target: android
           - os: windows-latest
             target: windows
           - os: macos-latest
@@ -48,6 +194,9 @@ jobs:
 
       - name: Setup Gradle
         uses: gradle/actions/setup-gradle@v4
+        with:
+          cache-read-only: false
+          gradle-home-cache-cleanup: true
 
       - name: Make gradlew executable
         shell: bash
@@ -63,7 +212,7 @@ jobs:
       # Decode the Android release keystore from a base64 secret (optional). Without the
       # secret the APK is debug-signed by the build fallback — still installable.
       - name: Decode Android keystore
-        if: matrix.os == 'ubuntu-latest'
+        if: matrix.target == 'android'
         shell: bash
         env:
           ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
@@ -76,48 +225,21 @@ jobs:
             echo "::warning::no ANDROID_KEYSTORE_BASE64 secret — APK will be debug-signed"
           fi
 
-      # ---------- linux desktop formats + android apk + reports (ubuntu) ----------
-      # One invocation with --continue so deb/rpm/AppImage/apk/reports each build to
-      # completion independently: a failure in one does NOT abort the others.
-      - name: Build linux desktop + android + reports
-        if: matrix.os == 'ubuntu-latest'
+      # ---------- android (.apk) ----------
+      - name: Build android .apk
+        if: matrix.target == 'android'
         shell: bash
-        continue-on-error: true
         env:
-          # ANDROID_KEYSTORE_FILE is exported by the decode step above (if present)
           ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
           ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
           ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
-        run: |
-          sudo apt-get update
-          sudo apt-get install -y rpm || echo "::warning::rpm tools missing — .rpm may be skipped"
-          ./gradlew packageFullDeb :desktopApp:packageReleaseRpm packageAppImageFile \
-            :androidApp:assembleRelease postBuildCodeCheck \
-            -PdepCheck=false --continue --stacktrace \
-            || echo "::warning::one or more linux/android formats failed — others still collected"
-
-      # ---------- bootable live ISO (ubuntu, needs root) ----------
-      # Separate step because makeIso needs root (debootstrap/chroot/mksquashfs); on CI it
-      # runs via passwordless sudo. Isolated so a failed/slow ISO can't drop the other formats.
-      - name: Build live ISO
-        if: matrix.os == 'ubuntu-latest'
-        shell: bash
-        continue-on-error: true
-        run: |
-          sudo apt-get install -y debootstrap squashfs-tools xorriso mtools dpkg-dev \
-            grub-common grub-pc-bin grub-efi-amd64-bin debian-archive-keyring \
-            || echo "::warning::iso toolchain missing — .iso may be skipped"
-          ./gradlew makeIso -PdepCheck=false --stacktrace \
-            || echo "::warning::makeIso failed — .iso skipped"
-          ls -la releases/*.iso 2>/dev/null && echo "ISO produced" \
-            || echo "::warning::no .iso in releases/ — makeIso skipped or failed"
+        run: ./gradlew :androidApp:assembleRelease -PdepCheck=false --stacktrace
 
       # ---------- windows (.exe) ----------
       - name: Build windows .exe
         if: matrix.os == 'windows-latest'
         shell: bash
-        continue-on-error: true
-        run: ./gradlew :desktopApp:packageReleaseExe -PdepCheck=false --stacktrace || echo "::warning::windows .exe failed — skipped"
+        run: ./gradlew :desktopApp:packageReleaseExe -PdepCheck=false --stacktrace
 
       # ---------- macos (.dmg) ----------
       # Signing/notarization activate only when the Apple Developer ID secrets are present
@@ -125,49 +247,76 @@ jobs:
       - name: Build macOS .dmg
         if: matrix.os == 'macos-latest'
         shell: bash
-        continue-on-error: true
         env:
           MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }}
           MACOS_BUNDLE_ID: ${{ secrets.MACOS_BUNDLE_ID }}
           MACOS_NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }}
           MACOS_NOTARY_PASSWORD: ${{ secrets.MACOS_NOTARY_PASSWORD }}
           MACOS_NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }}
-        run: ./gradlew :desktopApp:packageReleaseDmg -PdepCheck=false --stacktrace || echo "::warning::macOS .dmg failed — skipped"
+        run: ./gradlew :desktopApp:packageReleaseDmg -PdepCheck=false --stacktrace
 
       # ---------- collect whatever got produced (versioned names) ----------
-      # Copies from every known jpackage/build output dir; dirs absent on this OS are simply
-      # skipped. Mirrors the rename scheme of the :collectReleases Gradle task but tolerates
-      # partial builds so we never lose the formats that DID succeed.
+      # Primary source is releases/ (collectReleases + makeIso). Fall back to jpackage output
+      # dirs for partial builds and for platform-specific runners (exe/dmg).
       - name: Collect artifacts
+        if: always()
         shell: bash
         run: |
+          set -euo pipefail
           VER='${{ steps.meta.outputs.version }}'
           APP='${{ steps.meta.outputs.appname }}'
           BASE="$APP-$VER"
           PKG=packages/main-release
           mkdir -p upload
-          copy() { # <glob-dir> <ext>
-            for f in "$1"/*."$2"; do
-              [ -e "$f" ] && cp -v "$f" "upload/$BASE.$2" && return 0
+
+          copy_pkg() {
+            local dir="$1" ext="$2"
+            for f in "$dir"/*."$ext"; do
+              if [ -e "$f" ]; then
+                cp -v "$f" "upload/$BASE.$ext"
+                return 0
+              fi
             done
             return 0
           }
-          copy "$PKG/deb" deb
-          copy "$PKG/rpm" rpm
-          copy "$PKG/appimage" AppImage
-          copy "$PKG/exe" exe
-          copy "$PKG/dmg" dmg
-          for f in androidApp/build/outputs/apk/release/*.apk; do
-            [ -e "$f" ] && cp -v "$f" "upload/$BASE.apk" && break
-          done
-          for f in releases/*.iso; do
-            [ -e "$f" ] && cp -v "$f" "upload/$BASE.iso" && break
-          done
-          # reports tree zipped into the release
+
+          if compgen -G "releases/*" > /dev/null; then
+            cp -av releases/* upload/
+          fi
+
+          copy_pkg "$PKG/exe" exe
+          copy_pkg "$PKG/dmg" dmg
+          if [ ! -f "upload/$BASE.apk" ]; then
+            for f in androidApp/build/outputs/apk/release/*.apk; do
+              [ -e "$f" ] && cp -v "$f" "upload/$BASE.apk" && break
+            done
+          fi
+
           if [ -d reports ] && [ -n "$(ls -A reports 2>/dev/null)" ]; then
-            (cd reports && zip -r "../upload/$BASE-reports.zip" .) || echo "::warning::reports zip failed"
+            (cd reports && zip -r "../upload/$BASE-reports.zip" .)
           fi
-          echo "=== produced for ${{ matrix.target }} ==="; ls -la upload/ || true
+          echo "=== produced for ${{ matrix.target }} ==="
+          ls -la upload/
+
+          require_artifact() {
+            local file="upload/$1"
+            if [ ! -s "$file" ]; then
+              echo "::error::required release artifact is missing or empty: $1"
+              exit 1
+            fi
+          }
+
+          case '${{ matrix.target }}' in
+            android)
+              require_artifact "$BASE.apk"
+              ;;
+            windows)
+              require_artifact "$BASE.exe"
+              ;;
+            macos)
+              require_artifact "$BASE.dmg"
+              ;;
+          esac
 
       - name: Upload build artifacts
         if: always()
@@ -179,7 +328,9 @@ jobs:
 
   release:
     name: Publish GitHub Release
-    needs: package
+    needs:
+      - package-linux-iso
+      - package
     if: always()
     runs-on: ubuntu-latest
     permissions:
@@ -210,4 +361,4 @@ jobs:
           files: dist/**
           fail_on_unmatched_files: false
         env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 20 - 2
build.gradle.kts

@@ -147,6 +147,7 @@ val ensureAppImageTool = tasks.register<EnsureAppImageToolTask>("ensureAppImageT
     group = "mjdev"
     description = "Downloads appimagetool (single-file .AppImage builder) if not already present."
     toolPath.set(appImageToolPath)
+    outputs.file(appImageToolPath)
 }
 
 // Wraps the jpackage app-image directory into a single, CLI-runnable .AppImage file.
@@ -158,6 +159,8 @@ val packageAppImageFile = tasks.register<PackageAppImageTask>("packageAppImageFi
     appImagePath.set(pkgDirV.resolve("app/$appNameV").absolutePath)
     outputPath.set(pkgDirV.resolve("appimage/$appNameV.AppImage").absolutePath)
     toolPath.set(appImageToolPath)
+    inputs.dir(pkgDirV.resolve("app/$appNameV"))
+    outputs.file(pkgDirV.resolve("appimage/$appNameV.AppImage"))
 }
 
 // Turns the jpackage desktop .deb into a *complete, self-installable* package: injects the
@@ -174,6 +177,8 @@ val packageFullDeb = tasks.register<PackageFullDebTask>("packageFullDeb") {
     debDir.set(pkgDirV.resolve("deb").absolutePath)
     sessionDir.set(rootDir.resolve("compositor/build/session-install").absolutePath)
     runtimeDepends.set(compositorRuntimeDeps)
+    inputs.dir(rootDir.resolve("compositor/build/session-install"))
+    outputs.dir(pkgDirV.resolve("deb"))
 }
 
 // Copies all distributables into releases/ with version in the filename
@@ -208,7 +213,7 @@ val collectReleases = tasks.register<Copy>("collectReleases") {
 val makeIso = tasks.register("makeIso") {
     group = "mjdev"
     description = "Builds the minimal bootable mjdev desktop live ISO into releases/ (needs root / pkexec)."
-    dependsOn(":desktopApp:packageReleaseDeb", ":compositor:stageSession", packageFullDeb)
+    dependsOn(packageFullDeb)
     // capture plain Strings at configuration time — the doLast closure must not reference
     // script-level vals (that captures the script instance, which is null under the
     // configuration cache -> "Cannot invoke getDebDirV() because this$0 is null").
@@ -217,6 +222,10 @@ val makeIso = tasks.register("makeIso") {
     val debDirPath = pkgDirV.resolve("deb").absolutePath
     val sessionStagePath = rootDir.resolve("compositor/build/session-install").absolutePath
     val extraDebsPath = rootDir.resolve("deb-packages").absolutePath
+    inputs.dir(pkgDirV.resolve("deb"))
+    inputs.dir(rootDir.resolve("compositor/build/session-install"))
+    inputs.dir(rootDir.resolve("deb-packages"))
+    outputs.file(isoOutPath)
     doLast {
         val isoTools = listOf("debootstrap", "mksquashfs", "xorriso", "grub-mkrescue")
         // debootstrap/grub-mkrescue live in /usr/sbin — which is often absent from the Gradle
@@ -247,7 +256,12 @@ val makeIso = tasks.register("makeIso") {
             "--out", isoOutPath)
         logger.lifecycle("makeIso: ${args.joinToString(" ")}")
         val code = ProcessBuilder(args).inheritIO().start().waitFor()
-        check(code == 0) { "make-iso.sh failed (exit $code)" }
+        if (code != 0) {
+            logger.warn(
+                "::warning::makeIso: make-iso.sh failed (exit $code) — ISO skipped; " +
+                    "other distributables are still published",
+            )
+        }
     }
 }
 
@@ -262,6 +276,10 @@ tasks.register("runIsoQemu") {
     }
 }
 
+// ISO is built after collectReleases so deb/rpm/AppImage/apk land in releases/ even when
+// make-iso.sh is slow or fails; makeIso never fails the aggregate (warn-only on error).
+makeIso.configure { mustRunAfter(collectReleases) }
+
 val buildAll = tasks.register("buildAll") {
     group = "mjdev"
     description = "Builds all distributables this host can produce, collects them into releases/ (stable names), and generates reports into reports/ — like every build."

+ 4 - 0
desktopApp/desktopApp.pro

@@ -21,6 +21,9 @@
 -dontwarn com.couchbase.**
 -dontwarn com.google.**
 -dontwarn com.jogamp.**
+-dontwarn com.sun.prism.**
+-dontwarn com.sun.scenario.**
+-dontwarn com.sun.javafx.**
 -dontwarn javafx.**
 -dontwarn community.flock.**
 -dontwarn io.github.**
@@ -28,6 +31,7 @@
 -dontwarn io.lettuce.**
 -dontwarn io.netty.**
 -dontwarn io.opentelemetry.**
+-dontwarn java.lang.MatchException
 -dontwarn kotbase.**
 -dontwarn net.htmlparser.**
 -dontwarn okhttp3.internal.**

+ 42 - 0
last-ai-experiment.txt

@@ -0,0 +1,42 @@
+v readme.md projektu mas popis co by mela apka / jak by to melo vypadat.
+cilem je linux desktop environment inspirovan cinskym deepin linuxem z roku 2014 + me napady.
+dde desktop mel pravy dock a spodni dock.
+spodni dock mel dve rezimy: fill size aka windows kdy se
+roztahne bar na sirku okna, (v zakladnim stavu, protoze muze byt nadockovany na kteroukoli hranu obrazovky)
+a druhy rezim kde se na sirku v ose x roztahl do takove sirky aby byl obsah viditelnej.
+obsahem spodniho docku byla prvni ikona pro menu pak favorite apps a pak status tray.
+pri kliku na ikonu menu se zobrazi menu a to ma take dva rezimy, bud jako windows menu, anebo jako
+fullscreen touch menu podobne unity z ubuntu. tedy ikonky v mrizce.
+asi menu neni treba vysvetlovat. melo kategorie, search view, ikonku a umoznovalo listovat a spoustet aplikace.
+v tray byli ikonky tray, pak keyboard layout a hodinky a pod.
+v control center byli tabs kde se uzivatel dostal k veskerym nastavenim systemu, zvuk, wifi, obrazovka, temy, ...
+a bylo tam mozne nadokovat i vlastni custom panely.
+dal bych na desktopu chtel widgety ala screenlets z linuxu.
+ja mam v mem projektu zatim jen memory widget.
+na plose byli klasicky ikonky dle toho co obsahuje desktop folder uzivatele, resp Plocha.
+hezky v mrizce jako to dela kazdy jiny linux desktop.
+moje vychytavka je ze se cely system obarvi dle nastavene pozadi plochy, nebo jak to mam ted background
+rotuje z backgrounds folderu meni pozadi kazdyccccccccccch 60 sekund a vzdy kdyz se zmeni pozadi
+zmeni se i barvy vsech komponent systemu tak aby to vypadalo ze jsou soucasti obrazku, ktery je
+aktualne pouzit jako wallpaper.
+dal ze se vytvori gtk 2/3/4 tema, a vynuti se tak aby i okenka otevrenych aplikaci korenspondovala s
+pozadim a to i kdyz se po casovem intervale zmeni.
+text a ikonky a vsechno ma mit takovy kontrast a rozdil mezi barvami, aby byli lehce uzivatelem citelne.
+screenshoty desktopu s mou predstavou jsou ve slozce screenshots.
+jako ikonky se pouziva iconic ttf.
+vsechno co lze ma byti modifikovatelne a nastavitelne uzivatelem.
+
+a ted ukol:
+- vytvor novou branch s nazvem ai-desktop
+- do branche vytvor desktop z tohoto popisu a ze vseho co mas k dispozici v projektu, jakykoli
+soubor mimo te nesmi zajimat jen to co je v projektu, staci graficka cast a mock data, ktere bude ziskavat skrz managery jako to mam v projektu ja.
+- zadne hardcodes, pokud je potreba tak enumy nebo companion objekty v jinem souboru s konstantami.
+- vse co lze udelat modularne, manazery a helpery, nebo rozsiritelne veci musi byt pres interface a konkretni extendery.
+- jedna class jeden soubor.
+- kazda compose komponenta zvlast soubor.
+- kazdy enum zvlast soubor
+- multiplatform, zatim windows, linux, mac a android, ale predpripravit pro mobily
+
+jsem zvedav co vyplijes.
+tohle je posledni sance pro to ze uverim ze ai je uzitecna a nejde jen o ciry podvod a marketing.
+

+ 115 - 70
shared/src/desktopMain/kotlin/org/mjdev/desktop/managers/theme/linux/ThemeManagerLinux.kt

@@ -9,6 +9,7 @@
 package org.mjdev.desktop.managers.theme.linux
 
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.lerp
 import okio.Path
 import okio.Path.Companion.toPath
 import org.mjdev.desktop.context.IDesktopContext
@@ -73,11 +74,57 @@ class ThemeManagerLinux(
             createCssFile(gtk4CssFile)
             createCssFile(systemGtk3CssFile)
             createCssFile(systemGtk4CssFile)
+            // applyLiveGtkTheme()
         }.onFailure { e ->
             Log.e(e)
         }
     }
 
+    private fun applyLiveGtkTheme() {
+        val scheme =
+            if (palette.backgroundColor.isLightColor) {
+                COLOR_SCHEME_PREFER_LIGHT
+            } else {
+                COLOR_SCHEME_PREFER_DARK
+            }
+        runCatching {
+            // GTK_THEME env var overrides gsettings and blocks live reload — theme name must
+            // come only from gsettings so open apps pick up rewritten gtk.css on notify.
+            Shell.executeAndRead(
+                "gsettings",
+                "set",
+                "org.gnome.desktop.interface",
+                "color-scheme",
+                scheme,
+            )
+            Shell.executeAndRead(
+                "gsettings",
+                "set",
+                "org.gnome.desktop.interface",
+                "gtk-theme",
+                THEME_MJDEV,
+            )
+            // Brief hop away and back forces GTK to re-read ~/.themes/Mjdev and gtk.css.
+            Shell.executeAndRead(
+                "gsettings",
+                "set",
+                "org.gnome.desktop.interface",
+                "gtk-theme",
+                THEME_ADWAITA,
+            )
+            Shell.executeAndRead(
+                "gsettings",
+                "set",
+                "org.gnome.desktop.interface",
+                "gtk-theme",
+                THEME_MJDEV,
+            )
+            Log.d("Live GTK theme applied: $THEME_MJDEV color-scheme=$scheme")
+        }.onFailure { e ->
+            Log.w("Live GTK theme apply failed (gsettings unavailable?): ${e.message}")
+        }
+    }
+
     private fun createDesktopFile() =
         runAsync {
             runCatching {
@@ -112,6 +159,7 @@ class ThemeManagerLinux(
                 fgColor = palette.textColor
                 baseColor = palette.baseColor
                 textColor = palette.textColor
+                borderColor = palette.borderColor
                 selectedBgColor = palette.selectedBgColor
                 selectedFgColor = palette.selectedFgColor
                 tooltipBgColor = palette.tooltipBgColor
@@ -174,8 +222,20 @@ class ThemeManagerLinux(
         var buttonBgColor: Color = Color.SuperDarkGray
         var buttonFgColor: Color = Color.White
 
-        fun asString() =
-            """
+        fun asString(): String {
+            val buttonInsens = lerp(buttonBgColor, bgColor, 0.6f)
+            val backdropFg = lerp(bgColor, fgColor, 0.8f)
+            val backdropText = lerp(baseColor, textColor, 0.8f)
+            val backdropDarkFg = lerp(bgColor, fgColor, 0.75f)
+            val backdropDarkBg = lerp(bgColor, fgColor, 0.75f)
+            val backdropSelBg = bgColor.darker(0.08f)
+            return """
+            @define-color accent_color ${selectedBgColor.hexRgb};
+            @define-color accent_bg_color ${selectedBgColor.hexRgb};
+            @define-color accent_fg_color ${selectedFgColor.hexRgb};
+            @define-color destructive_color ${errorBgColor.hexRgb};
+            @define-color destructive_bg_color ${errorBgColor.hexRgb};
+            @define-color destructive_fg_color ${errorFgColor.hexRgb};
             @define-color bg_color ${bgColor.hexRgb};
             @define-color fg_color ${fgColor.hexRgb};
             @define-color base_color ${baseColor.hexRgb};
@@ -216,18 +276,18 @@ class ThemeManagerLinux(
             @define-color popover_bg_color ${bgColor.hexRgb};
             @define-color popover_fg_color ${fgColor.hexRgb};
             @define-color link_color @selected_bg_color;
-            @define-color button_bg_color shade (@bg_color, 1.02);
-            @define-color notebook_button_bg_color shade (@bg_color, 1.02);
-            @define-color button_insensitive_bg_color mix (@button_bg_color, @bg_color, 0.6);
-            @define-color backdrop_fg_color mix (@bg_color, @fg_color, 0.8);
-            @define-color backdrop_text_color mix (@base_color, @text_color, 0.8);
-            @define-color backdrop_dark_fg_color mix (@dark_bg_color, @dark_fg_color, 0.75);
-            @define-color backdrop_dark_bg_color mix (@dark_bg_color, @dark_fg_color, 0.75);
-            @define-color backdrop_selected_bg_color shade (@bg_color, 0.92);
+            @define-color button_bg_color ${buttonBgColor.hexRgb};
+            @define-color notebook_button_bg_color ${buttonBgColor.hexRgb};
+            @define-color button_insensitive_bg_color ${buttonInsens.hexRgb};
+            @define-color backdrop_fg_color ${backdropFg.hexRgb};
+            @define-color backdrop_text_color ${backdropText.hexRgb};
+            @define-color backdrop_dark_fg_color ${backdropDarkFg.hexRgb};
+            @define-color backdrop_dark_bg_color ${backdropDarkBg.hexRgb};
+            @define-color backdrop_selected_bg_color ${backdropSelBg.hexRgb};
             @define-color backdrop_selected_fg_color @fg_color;
-            @define-color focus_color alpha (@selected_bg_color, 0.5);
-            @define-color focus_bg_color alpha (@selected_bg_color, 0.1);
-            @define-color shadow_color alpha(black, 0.5);
+            @define-color focus_color ${selectedBgColor.hexRgb};
+            @define-color focus_bg_color ${bgColor.hexRgb};
+            @define-color shadow_color rgba(0, 0, 0, 0.5);
             @define-color blue_1 #99c1f1;
             @define-color blue_2 #62a0ea;
             @define-color blue_3 #3584e4;
@@ -274,89 +334,73 @@ class ThemeManagerLinux(
             @define-color dark_4 #241f31;
             @define-color dark_5 #000000;
             
+            /* App content follows the wallpaper palette (compositor draws the outer frame). */
             window {
                 background-image: none;
-                background-color: ${bgColor.hexRgb};
-            	border-radius: 8px;
-            	border-bottom-left-radius: 8px;
-            	border-bottom-right-radius: 8px;
-                border-top-right-radius: 8px;
-                border-top-left-radius: 8px;
-                border: 2px solid ${bgColor.hexRgb};
-            	border-top: none;
-                /* position: relative; */
-                box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.3), 0 0 40px rgba(0, 0, 0, 0.1) inset;
+                background-color: @window_bg_color;
+                border-radius: 0;
+                box-shadow: none;
+                border: none;
+                margin: 0;
+                padding: 0;
             }
-            
-            window:before, window:after {
-                /* position:absolute; */
-                z-index: -1;
-                box-shadow: 0 0 20px rgba(0, 0, 0, 0.8);
-                top: 0;
-                bottom: 0;
-                left: 10px;
-                right: 10px;
-                border-radius: 100px / 10px;
-            }
-            
-            window:after {
-                right: 10px;
-                left: auto;
-                transform: skew(8deg) rotate(3deg);
+
+            /* Compositor draws min/max/close — hide client-side titlebuttons only. */
+            headerbar button.titlebutton,
+            headerbar .titlebutton {
+                opacity: 0;
+                min-width: 0;
+                min-height: 0;
+                padding: 0;
+                margin: 0;
+                border: none;
             }
 
-            decoration {
+            window.ssd headerbar {
+                min-height: 0;
+                padding: 0;
+                margin: 0;
+                border: none;
+                box-shadow: none;
                 background-image: none;
-            	background-color: ${bgColor.hexRgb};
-            	border-radius: 8px;
-            	border-bottom-left-radius: 8px;
-            	border-bottom-right-radius: 8px;
-            	box-shadow: 0px 0px 0px 1px ${textColor.hexRgb};
-            	border-top: none;
+                background-color: @window_bg_color;
             }
-            
+
             .titlebar, headerbar {
                 padding-top: 2px;
                 padding-bottom: 2px;
                 background-image: none;
-                background-color: ${bgColor.hexRgb};
+                background-color: @headerbar_bg_color;
             }
 
-            .titlebar:backdrop, headerbar:backdrop  {
-                background: ${bgColor.hexRgb};
-                color: ${textColor.hexRgb};
+            toolbarview,
+            .toolbarview,
+            .background {
+                background-color: @window_bg_color;
+                color: @text_color;
             }
-            
-            /* all buttons */
+
+            entry, textview, label {
+                color: @text_color;
+            }
+
             button {
-                background: ${buttonBgColor.hexRgb};
-                opacity: 0.7;
+                background-color: @button_bg_color;
+                opacity: 0.85;
                 margin: 2px;
                 padding: 4px;
                 min-width: 24px;
                 min-height: 24px;
                 text-shadow: none;
-                color:  ${buttonFgColor.hexRgb};
+                color: ${buttonFgColor.hexRgb};
                 border-radius: 8px;
             }
-            
-            /* window buttons */
-            button.minimize,
-            button.maximize,
-            button.close,
-            button.maximize:hover,
-            button.minimize:hover,
-            button.close:hover {
-                opacity: 0.7;
-            }
 
-            button:hover,
-            button.maximize:hover,
-            button.minimize:hover,
-            button.close:hover {
+            button:hover {
                 opacity: 1;
             }
             """.trimIndent()
+        }
     }
 
     fun setColorScheme(schemeName: String) {
@@ -421,6 +465,7 @@ class ThemeManagerLinux(
 
     companion object {
         const val COLOR_SCHEME_PREFER_DARK = "prefer-dark"
+        const val COLOR_SCHEME_PREFER_LIGHT = "prefer-light"
         const val COLOR_SCHEME_MJDEV = "mjdev"
         const val THEME_YARU = "Yaru"
         const val THEME_ADWAITA = "Adwaita"