Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions app/src/desktopMain/kotlin/com/crosspaste/DesktopMouseModule.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.crosspaste

import com.crosspaste.config.DesktopConfigManager
import com.crosspaste.config.DevConfig
import com.crosspaste.mouse.AwtLocalScreensProvider
import com.crosspaste.mouse.LocalScreensProvider
import com.crosspaste.mouse.MacosLocalScreensProvider
Expand All @@ -12,9 +13,11 @@ import com.crosspaste.mouse.MouseIpcProtocol
import com.crosspaste.mouse.MouseLayoutStore
import com.crosspaste.mouse.Position
import com.crosspaste.mouse.asDaemonHandle
import com.crosspaste.path.AppPathProvider
import com.crosspaste.platform.Platform
import com.crosspaste.sync.SyncManager
import com.crosspaste.ui.mouse.ScreenArrangementViewModel
import com.crosspaste.utils.getAppEnvUtils
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
Expand Down Expand Up @@ -116,9 +119,38 @@ fun desktopMouseModule(): Module =
// SyncManager's StateFlow is the canonical multi-cast view.
syncRuntimeInfosFlow = get<SyncManager>().realTimeSyncRuntimeInfos,
clientFactory = {
val platform = get<Platform>()
val pathProvider = get<AppPathProvider>()
val isDevelopment = getAppEnvUtils().isDevelopment()

// Dev: developer-supplied path from development.properties.
// Prod: bundled binary sits next to the app's runtime under pasteAppExePath.
val candidates =
if (isDevelopment) {
listOfNotNull(DevConfig.mouseBinaryPath)
} else {
listOf(
pathProvider.pasteAppExePath.resolve(MouseDaemonBinary.binaryName(platform)).toString(),
)
}

val binary =
MouseDaemonBinary.resolve()
?: throw IllegalStateException("crosspaste-mouse binary not found")
MouseDaemonBinary.resolve(candidatePaths = candidates)
?: throw IllegalStateException(
buildString {
append("crosspaste-mouse binary not found. Tried: ")
append(if (candidates.isEmpty()) "(no candidates)" else candidates.joinToString())
if (isDevelopment) {
append(". Set mouseBinaryPath in development.properties, ")
append("or set CROSSPASTE_MOUSE_BIN / -Dcrosspaste.mouse.binary.")
} else {
append(
". The bundled binary is missing — please reinstall the app or " +
"report this issue.",
)
}
},
)
MouseDaemonClient(MouseDaemonProcess.spawn(binary).asDaemonHandle())
},
)
Expand Down
2 changes: 2 additions & 0 deletions app/src/desktopMain/kotlin/com/crosspaste/config/DevConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ object DevConfig {
val pasteUserPath: String? = development.getProperty("pasteUserPath")

val marketingMode: Boolean = development.getProperty("marketingMode")?.toBoolean() == true

val mouseBinaryPath: String? = development.getProperty("mouseBinaryPath")?.takeIf { it.isNotBlank() }
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.crosspaste.mouse

import com.crosspaste.platform.Platform
import java.io.File

object MouseDaemonBinary {
Expand All @@ -8,27 +9,29 @@ object MouseDaemonBinary {

/**
* Resolution order:
* 1. -Dcrosspaste.mouse.binary=<path>
* 2. $CROSSPASTE_MOUSE_BIN
* 3. Any path in `candidatePaths` (e.g. bundled in resources/bin/...)
* Returns null if nothing points at an existing regular file.
* 1. -Dcrosspaste.mouse.binary=<path> (per-launch override)
* 2. $CROSSPASTE_MOUSE_BIN (per-shell override)
* 3. Any path in [candidatePaths] (caller-supplied, env-aware)
*
* The caller is responsible for assembling [candidatePaths] from the
* dev-mode `DevConfig.mouseBinaryPath` or the prod-mode
* `pasteAppExePath / binaryName(platform)`. Returns null if nothing
* points at an existing regular file.
*/
fun resolve(
candidatePaths: List<String> = emptyList(),
envLookup: (String) -> String? = System::getenv,
candidatePaths: List<String> = defaultCandidatePaths(),
): File? {
System.getProperty(SYSTEM_PROPERTY)?.let { File(it).takeIf { f -> f.isFile } }?.let { return it }
envLookup(ENV_VAR)?.let { File(it).takeIf { f -> f.isFile } }?.let { return it }
return candidatePaths
.asSequence()
.filter { it.isNotBlank() }
.map(::File)
.firstOrNull { it.isFile }
}

private fun defaultCandidatePaths(): List<String> {
// Production bundling is a follow-up plan; for now the only
// candidate is a dev-mode symlink next to the app jar.
val home = System.getProperty("user.home") ?: return emptyList()
return listOf("$home/crosspaste-mouse/target/release/crosspaste-mouse")
}
/** crosspaste-mouse build artifact name for the given platform. */
fun binaryName(platform: Platform): String =
if (platform.isWindows()) "crosspaste-mouse.exe" else "crosspaste-mouse"
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
pasteAppPath=.
pasteUserPath=.user
pasteUserPath=.user

# Mouse daemon binary path (development only). Set to the absolute path of
# your local crosspaste-mouse build. Leave empty to fall back to
# -Dcrosspaste.mouse.binary or $CROSSPASTE_MOUSE_BIN.
# Example: mouseBinaryPath=/Users/me/crosspaste-mouse/target/release/crosspaste-mouse
mouseBinaryPath=
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.crosspaste.mouse

import com.crosspaste.platform.Platform
import java.nio.file.Files
import kotlin.test.AfterTest
import kotlin.test.Test
Expand Down Expand Up @@ -50,4 +51,26 @@ class MouseDaemonBinaryTest {
System.setProperty(propKey, "/definitely/does/not/exist/mouse")
assertNull(MouseDaemonBinary.resolve(envLookup = { null }, candidatePaths = emptyList()))
}

@Test
fun `binaryName appends exe on windows only`() {
val windows = Platform(name = "Windows", arch = "x64", bitMode = 64, version = "10")
val macos = Platform(name = "Macos", arch = "arm64", bitMode = 64, version = "14")
val linux = Platform(name = "Linux", arch = "x64", bitMode = 64, version = "5.15")
assertEquals("crosspaste-mouse.exe", MouseDaemonBinary.binaryName(windows))
assertEquals("crosspaste-mouse", MouseDaemonBinary.binaryName(macos))
assertEquals("crosspaste-mouse", MouseDaemonBinary.binaryName(linux))
}

@Test
fun `candidate paths are used when overrides are absent`() {
System.clearProperty(propKey)
val file = Files.createTempFile("fake-daemon", "").toFile().apply { deleteOnExit() }
val result =
MouseDaemonBinary.resolve(
candidatePaths = listOf("", "/does/not/exist", file.absolutePath),
envLookup = { null },
)
assertEquals(file.absolutePath, result?.absolutePath)
}
}
Loading