From a2b37e466fa2e013d5e9be24cf24cc2ff5e13ab1 Mon Sep 17 00:00:00 2001 From: "Elie G." Date: Fri, 25 Sep 2026 14:36:30 +0300 Subject: [PATCH 1/2] feat(share): add a Kotlin Multiplatform native share sheet module Android and iOS in pure Kotlin, desktop through a Rust JNI bridge (Windows DataTransferManager, macOS NSSharingServicePicker, Linux XDG portal), inspired by robius-share. nucleus-application gains NucleusWindow.share, which resolves the parent window (and the Wayland export's lifetime) from the Tao window. --- .github/workflows/build-natives.yaml | 16 + .github/workflows/pre-merge.yaml | 6 + .github/workflows/publish-maven.yaml | 6 + CLAUDE.md | 3 +- README.md | 1 + THIRD_PARTY_NOTICES.md | 25 +- build.gradle.kts | 15 + .../gradle/NativeModulePlugin.kt | 25 +- examples/share-demo/build.gradle.kts | 33 + .../src/main/kotlin/com/example/share/Main.kt | 142 ++++ gradle/libs.versions.toml | 3 + licenses/LICENSE-MIT-robius.txt | 21 + .../api/nucleus-application.api | 8 + nucleus-application/build.gradle.kts | 4 + .../dev/nucleusframework/application/Share.kt | 99 +++ settings.gradle.kts | 2 + share/README.md | 88 +++ share/api/jvm/share.api | 185 +++++ share/build.gradle.kts | 116 +++ share/src/androidMain/AndroidManifest.xml | 11 + .../share/NucleusShareProvider.kt | 156 +++++ .../share/ShareSheet.android.kt | 154 ++++ .../dev/nucleusframework/share/MimeTypes.kt | 25 + .../nucleusframework/share/ShareException.kt | 41 ++ .../dev/nucleusframework/share/ShareItem.kt | 42 ++ .../nucleusframework/share/ShareRequest.kt | 92 +++ .../dev/nucleusframework/share/ShareSheet.kt | 54 ++ .../share/ShareRequestTest.kt | 88 +++ .../nucleusframework/share/ShareSheet.ios.kt | 69 ++ .../share/NativeShareBridge.kt | 73 ++ .../dev/nucleusframework/share/ShareParent.kt | 52 ++ .../nucleusframework/share/ShareSheet.jvm.kt | 55 ++ .../nucleus.share/reachability-metadata.json | 18 + .../share/ShareParentLeaseTest.kt | 20 + share/src/main/native/Cargo.lock | 555 +++++++++++++++ share/src/main/native/Cargo.toml | 50 ++ share/src/main/native/linux/build.sh | 71 ++ share/src/main/native/macos/build.sh | 74 ++ share/src/main/native/src/completion.rs | 54 ++ share/src/main/native/src/error.rs | 63 ++ share/src/main/native/src/lib.rs | 190 +++++ .../main/native/src/platform/linux/dbus.rs | 660 ++++++++++++++++++ .../src/main/native/src/platform/linux/mod.rs | 438 ++++++++++++ share/src/main/native/src/platform/macos.rs | 94 +++ share/src/main/native/src/platform/mod.rs | 19 + share/src/main/native/src/platform/windows.rs | 228 ++++++ share/src/main/native/windows/build.bat | 111 +++ 47 files changed, 4349 insertions(+), 6 deletions(-) create mode 100644 examples/share-demo/build.gradle.kts create mode 100644 examples/share-demo/src/main/kotlin/com/example/share/Main.kt create mode 100644 licenses/LICENSE-MIT-robius.txt create mode 100644 nucleus-application/src/main/kotlin/dev/nucleusframework/application/Share.kt create mode 100644 share/README.md create mode 100644 share/api/jvm/share.api create mode 100644 share/build.gradle.kts create mode 100644 share/src/androidMain/AndroidManifest.xml create mode 100644 share/src/androidMain/kotlin/dev/nucleusframework/share/NucleusShareProvider.kt create mode 100644 share/src/androidMain/kotlin/dev/nucleusframework/share/ShareSheet.android.kt create mode 100644 share/src/commonMain/kotlin/dev/nucleusframework/share/MimeTypes.kt create mode 100644 share/src/commonMain/kotlin/dev/nucleusframework/share/ShareException.kt create mode 100644 share/src/commonMain/kotlin/dev/nucleusframework/share/ShareItem.kt create mode 100644 share/src/commonMain/kotlin/dev/nucleusframework/share/ShareRequest.kt create mode 100644 share/src/commonMain/kotlin/dev/nucleusframework/share/ShareSheet.kt create mode 100644 share/src/commonTest/kotlin/dev/nucleusframework/share/ShareRequestTest.kt create mode 100644 share/src/iosMain/kotlin/dev/nucleusframework/share/ShareSheet.ios.kt create mode 100644 share/src/jvmMain/kotlin/dev/nucleusframework/share/NativeShareBridge.kt create mode 100644 share/src/jvmMain/kotlin/dev/nucleusframework/share/ShareParent.kt create mode 100644 share/src/jvmMain/kotlin/dev/nucleusframework/share/ShareSheet.jvm.kt create mode 100644 share/src/jvmMain/resources/META-INF/native-image/dev.nucleusframework/nucleus.share/reachability-metadata.json create mode 100644 share/src/jvmTest/kotlin/dev/nucleusframework/share/ShareParentLeaseTest.kt create mode 100644 share/src/main/native/Cargo.lock create mode 100644 share/src/main/native/Cargo.toml create mode 100644 share/src/main/native/linux/build.sh create mode 100644 share/src/main/native/macos/build.sh create mode 100644 share/src/main/native/src/completion.rs create mode 100644 share/src/main/native/src/error.rs create mode 100644 share/src/main/native/src/lib.rs create mode 100644 share/src/main/native/src/platform/linux/dbus.rs create mode 100644 share/src/main/native/src/platform/linux/mod.rs create mode 100644 share/src/main/native/src/platform/macos.rs create mode 100644 share/src/main/native/src/platform/mod.rs create mode 100644 share/src/main/native/src/platform/windows.rs create mode 100644 share/src/main/native/windows/build.bat diff --git a/.github/workflows/build-natives.yaml b/.github/workflows/build-natives.yaml index 02f288f2d..886a92542 100644 --- a/.github/workflows/build-natives.yaml +++ b/.github/workflows/build-natives.yaml @@ -120,6 +120,11 @@ jobs: shell: cmd run: call fs-watcher\src\main\native\windows\build.bat + - name: Build share Windows DLLs + if: steps.natives-cache.outputs.cache-hit != 'true' + shell: cmd + run: call share\src\main\native\windows\build.bat + - name: Build decorated-window-tao Windows DLLs if: steps.natives-cache.outputs.cache-hit != 'true' shell: cmd @@ -144,6 +149,7 @@ jobs: "media-control/nucleus_media_control_windows.dll" "spellcheck/nucleus_spellcheck.dll" "fs-watcher/nucleus_fs_watcher.dll" + "share/nucleus_share.dll" "decorated-window-tao/nucleus_tao.dll" "decorated-window-tao/nucleus_tao_windows_deco.dll" "decorated-window-tao/nucleus_tao_gl.dll" @@ -263,6 +269,10 @@ jobs: if: steps.natives-cache.outputs.cache-hit != 'true' run: bash fs-watcher/src/main/native/macos/build.sh + - name: Build share macOS dylibs + if: steps.natives-cache.outputs.cache-hit != 'true' + run: bash share/src/main/native/macos/build.sh + - name: Build decorated-window-tao macOS dylibs if: steps.natives-cache.outputs.cache-hit != 'true' run: bash decorated-window-tao/src/main/native/macos/build.sh @@ -290,6 +300,7 @@ jobs: "media-control/libnucleus_media_control_macos.dylib" "spellcheck/libnucleus_spellcheck.dylib" "fs-watcher/libnucleus_fs_watcher.dylib" + "share/libnucleus_share.dylib" "decorated-window-tao/libnucleus_tao.dylib" "decorated-window-tao/libnucleus_tao_metal.dylib" "decorated-window-tao/libnucleus_tao_dnd.dylib" @@ -423,6 +434,10 @@ jobs: if: steps.natives-cache.outputs.cache-hit != 'true' run: bash fs-watcher/src/main/native/linux/build.sh + - name: Build share Linux native libraries + if: steps.natives-cache.outputs.cache-hit != 'true' + run: bash share/src/main/native/linux/build.sh + - name: Build decorated-window-tao Linux native libraries if: steps.natives-cache.outputs.cache-hit != 'true' run: bash decorated-window-tao/src/main/native/linux/build.sh @@ -444,6 +459,7 @@ jobs: "scheduler/libnucleus_scheduler_linux.so" "media-control/libnucleus_media_control_linux.so" "fs-watcher/libnucleus_fs_watcher.so" + "share/libnucleus_share.so" "decorated-window-tao/libnucleus_tao.so" "decorated-window-tao/libnucleus_tao_egl.so" "decorated-window-tao/libnucleus_tao_linux_widget.so" diff --git a/.github/workflows/pre-merge.yaml b/.github/workflows/pre-merge.yaml index 008507af7..fc63ba973 100644 --- a/.github/workflows/pre-merge.yaml +++ b/.github/workflows/pre-merge.yaml @@ -84,6 +84,12 @@ jobs: "fs-watcher/src/main/resources/nucleus/native/darwin-x64/libnucleus_fs_watcher.dylib" "fs-watcher/src/main/resources/nucleus/native/linux-x64/libnucleus_fs_watcher.so" "fs-watcher/src/main/resources/nucleus/native/linux-aarch64/libnucleus_fs_watcher.so" + "share/src/main/resources/nucleus/native/win32-x64/nucleus_share.dll" + "share/src/main/resources/nucleus/native/win32-aarch64/nucleus_share.dll" + "share/src/main/resources/nucleus/native/darwin-aarch64/libnucleus_share.dylib" + "share/src/main/resources/nucleus/native/darwin-x64/libnucleus_share.dylib" + "share/src/main/resources/nucleus/native/linux-x64/libnucleus_share.so" + "share/src/main/resources/nucleus/native/linux-aarch64/libnucleus_share.so" "notification-macos/src/main/resources/nucleus/native/darwin-aarch64/libnucleus_notification.dylib" "notification-macos/src/main/resources/nucleus/native/darwin-x64/libnucleus_notification.dylib" "service-management-macos/src/main/resources/nucleus/native/darwin-aarch64/libnucleus_service_management.dylib" diff --git a/.github/workflows/publish-maven.yaml b/.github/workflows/publish-maven.yaml index 85d3f6c31..3a5c45ae5 100644 --- a/.github/workflows/publish-maven.yaml +++ b/.github/workflows/publish-maven.yaml @@ -86,6 +86,12 @@ jobs: "fs-watcher/src/main/resources/nucleus/native/darwin-x64/libnucleus_fs_watcher.dylib" "fs-watcher/src/main/resources/nucleus/native/linux-x64/libnucleus_fs_watcher.so" "fs-watcher/src/main/resources/nucleus/native/linux-aarch64/libnucleus_fs_watcher.so" + "share/src/main/resources/nucleus/native/win32-x64/nucleus_share.dll" + "share/src/main/resources/nucleus/native/win32-aarch64/nucleus_share.dll" + "share/src/main/resources/nucleus/native/darwin-aarch64/libnucleus_share.dylib" + "share/src/main/resources/nucleus/native/darwin-x64/libnucleus_share.dylib" + "share/src/main/resources/nucleus/native/linux-x64/libnucleus_share.so" + "share/src/main/resources/nucleus/native/linux-aarch64/libnucleus_share.so" "notification-macos/src/main/resources/nucleus/native/darwin-aarch64/libnucleus_notification.dylib" "notification-macos/src/main/resources/nucleus/native/darwin-x64/libnucleus_notification.dylib" "service-management-macos/src/main/resources/nucleus/native/darwin-aarch64/libnucleus_service_management.dylib" diff --git a/CLAUDE.md b/CLAUDE.md index eb560a4a2..531134633 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,6 +31,7 @@ Published releases are `2.5.x` (latest tag `v2.5.0`). Do not treat `IDEAL_API.md - `autolaunch` - Start at login (Win32/MSIX/SMAppService/systemd/Flatpak portal) - `scheduler` / `scheduler-testing` - OS-scheduled background tasks (Task Scheduler / launchd / systemd) + test doubles - `fs-watcher` - Native filesystem watcher over the Rust `notify` crate. One native watcher per `FsWatcher` (all registrations share it; events are routed back by root in `lib.rs`, so an inotify instance is per watcher, not per path — #571). The backend is handed the canonical root on macOS and Linux (one watch per real directory; inotify keys watches by inode and `notify` keeps one spelling per descriptor, so aliases must share it — Kotlin projects events back onto each registration's spelling), the registered spelling on Windows (`canonicalize()` yields `\\?\` paths there). On macOS the Rust side also feeds FSEvents through `FsEventsNormalizer` before the debouncer: FSEvents reports an inode's *accumulated* flags (a rename of an old file arrives as `Create`+`Rename`+`Modify` on the gone path), which otherwise folds renames into a bare `Created(new)` and deletes into `Modified` (#570). Renames the backend cannot pair are `Removed(old)` + `Created(new)`, never dropped; `Raw` delivery never emits `Moved` +- `share` - Native share sheet, the repo's first **Kotlin Multiplatform library** (inspired by `robius-share`, THIRD_PARTY_NOTICES §5). `commonMain` holds the API (`ShareSheet.share { title; text(); url(); file() }`, suspend, throws `ShareException(ShareError)`); Android (`Intent.ACTION_SEND` chooser) and iOS (`UIActivityViewController`) are pure Kotlin, only the `jvm` target goes through the Rust JNI crate (Windows `DataTransferManager` interop, macOS `NSSharingServicePicker`, Linux XDG portal over a hand-rolled D-Bus client). The KMP module keeps the JVM JNI layout (`src/main/native`, `src/main/resources` added to `jvmMain`) so CI globs stay uniform; `nucleus.native-module` wires `jvmProcessResources` when the multiplatform plugin is applied (target must be named `jvm`). Android files are copied under `cacheDir/nucleus-share` and served by the library's own `NucleusShareProvider` (`${applicationId}.nucleus.share`, no AndroidX), which also tracks the resumed activity. Desktop parents: `ShareSheet.share(request, ShareParent.Windows(hwnd) / MacOs(nsWindow, anchor) / Linux(portalParent, keepAlive))`; apps on `nucleus-application` call `NucleusWindow.share(request, anchor)` (`compileOnly` dependency, like FileKit), which resolves them from the Tao window. A Wayland `xdg_foreign` export must outlive the portal dialog while `share` returns before it closes: `keepAlive` is handed to the crate as a `java.lang.Runnable` (`Completion`, run on `Drop`, so every path releases it) that the Linux portal thread drops once the portal answers; the Kotlin side closes it itself (once) when the share fails or is cancelled before the native call. Kotlin 2.4 compiles the iOS klibs on any host, so iOS code is checked on Windows too; in a KMP module the aggregate `detekt` is NO-SOURCE — the root build wires the per-source-set `detekt*MainSourceSet` tasks into `check` - `service-management-macos` - macOS `SMAppService` — login items, launch agents, daemons - `native-ssl` / `native-http` / `native-http-okhttp` / `native-http-ktor` - OS trust store integration - `linux-hidpi` - Native HiDPI scale detection on Linux @@ -43,7 +44,7 @@ Published releases are `2.5.x` (latest tag `v2.5.0`). Do not treat `IDEAL_API.md - `decorated-window-material3` - Material 3 color mapping - `plugin-build/plugin` - Gradle plugin for packaging & distribution - `buildSrc` - Build-only convention plugins (`nucleus.native-module`: the shared `buildNative*` wiring for every JNI module) -- `examples/` - Demo & sample applications: `nucleus-demo` (flagship), `compose-demo`, `tao-demo`, `swing-tao-demo`, `jewel-demo`, `cmp-demo` (KMP), `window-scaffold-demo`, `satellite-demo` (satellite workspace: floating palettes following the focused document, docking into a `DockLayout`, drag-to-dock, layout snapshots), `tabs-demo` (Chrome-like tabs: tear-off, merge, reorder, state following a tab between windows, layout snapshots), `jewel-tabs-demo` (the same tab workspace wearing Jewel's `TabStrip` / `TabData.Editor` chrome), `tab-satellites-demo` (the two archetypes composed: one `SatelliteWorkspace` per tab window, palettes drawing the window's selected tab), `reader-dock-demo` (a right-to-left book reader composing **both** archetypes: its seforim are tabs and its every pane is a satellite — layered right side with per-pane widths, `sideOrder` putting the right side outside the bottom one, the reader's own 1 dp + 5 dp-grip splitters and hover headers, Classic/Islands styles, one dock per tab window hung on `TabWindows(windowBodyWrapper)` so the strip stays the top of the window and a tab change touches no panel — the target layout of SeforimApp), `zstd-demo`, `scheduler-demo`, `service-management-demo`, `system-info-demo`, `fs-watcher-smoke`, `orphan-reflect-smoke`, `extra-launcher-demo`, `macos-appex-demo` (a macOS Network Extension `.appex` embedded and signed through `macOS { appExtensions { } }`), `tao-native-test` (GraalVM + SLF4J fixture), `benchmark-demo` (JIT-vs-GraalVM-O3, ports under `ports/`), `gstreamer-demo` / `mediafoundation-demo` / `avfoundation-demo` (platform video into a `TextureView`), plus `shared` (Compose helper used by the tao demos). `native-proxy` and `spellcheck` directories on disk are **not** on `main` — ignore them unless the matching feature branch is checked out. +- `examples/` - Demo & sample applications: `nucleus-demo` (flagship), `compose-demo`, `tao-demo`, `swing-tao-demo`, `jewel-demo`, `cmp-demo` (KMP), `window-scaffold-demo`, `satellite-demo` (satellite workspace: floating palettes following the focused document, docking into a `DockLayout`, drag-to-dock, layout snapshots), `tabs-demo` (Chrome-like tabs: tear-off, merge, reorder, state following a tab between windows, layout snapshots), `jewel-tabs-demo` (the same tab workspace wearing Jewel's `TabStrip` / `TabData.Editor` chrome), `tab-satellites-demo` (the two archetypes composed: one `SatelliteWorkspace` per tab window, palettes drawing the window's selected tab), `reader-dock-demo` (a right-to-left book reader composing **both** archetypes: its seforim are tabs and its every pane is a satellite — layered right side with per-pane widths, `sideOrder` putting the right side outside the bottom one, the reader's own 1 dp + 5 dp-grip splitters and hover headers, Classic/Islands styles, one dock per tab window hung on `TabWindows(windowBodyWrapper)` so the strip stays the top of the window and a tab change touches no panel — the target layout of SeforimApp), `share-demo` (every `ShareSheet` payload on the desktop bridge; `-Dshare.demo.auto=