Skip to content

Commit

Permalink
Merge pull request #59 from aivanovski/feature/add-xwayland-compatibi…
Browse files Browse the repository at this point in the history
…lity

Add XWayland compatibility
  • Loading branch information
aivanovski committed Nov 8, 2023
2 parents 94036eb + e8e96f5 commit a86a39c
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .github/badges/jacoco.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion src/main/java/com/github/ai/autokpass/di/KoinModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.github.ai.autokpass.domain.formatter.EntryFormatter
import com.github.ai.autokpass.domain.fuzzySearch.FuzzyMatcher
import com.github.ai.autokpass.domain.fuzzySearch.Fzf4jFuzzyMatcher
import com.github.ai.autokpass.domain.usecases.DetermineAutotypeExecutorTypeUseCase
import com.github.ai.autokpass.domain.usecases.DetermineDesktopUseCase
import com.github.ai.autokpass.domain.usecases.GetOSTypeUseCase
import com.github.ai.autokpass.domain.usecases.GetVisibleEntriesUseCase
import com.github.ai.autokpass.domain.usecases.PrintGreetingsUseCase
Expand Down Expand Up @@ -86,13 +87,14 @@ object KoinModule {
single { GetOSTypeUseCase(get(), get()) }
single { DetermineAutotypeExecutorTypeUseCase(get()) }
single { KeepassDatabaseFactoryProvider(get(), get(), get()) }
single { DetermineDesktopUseCase(get()) }

// interactors
single { StartInteractor(get(), get()) }
single<UnlockInteractor> { UnlockInteractorImpl(get(), get(), get()) }
single<SelectEntryInteractor> { SelectEntryInteractorImpl(get(), get(), get(), get()) }
single<SelectPatternInteractor> { SelectPatternInteractorImpl(get(), get(), get(), get()) }
single { AutotypeInteractor(get(), get(), get(), get(), get(), get(), get()) }
single { AutotypeInteractor(get(), get(), get(), get(), get(), get(), get(), get()) }

// View Models
factory { (router: Router) ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.github.ai.autokpass.domain.usecases

import com.github.ai.autokpass.model.DesktopType
import com.github.ai.autokpass.model.Result
import com.github.ai.autokpass.presentation.process.ProcessExecutor
import com.github.ai.autokpass.util.StringUtils.NEW_LINE

class DetermineDesktopUseCase(
private val processExecutor: ProcessExecutor
) {

fun getDesktopType(): Result<DesktopType> {
val environmentOutputResult = processExecutor.execute(COMMAND)
if (environmentOutputResult.isFailed()) {
return environmentOutputResult.asErrorOrThrow()
}

val environment = environmentOutputResult.getDataOrThrow()
.parseEnvironmentOutput()

return Result.Success(classifyDesktopEnvironment(environment))
}

private fun String.parseEnvironmentOutput(): Map<String, String> {
return this.split(NEW_LINE)
.map { line -> line.trim() }
.filter { line -> line.isNotEmpty() && line.contains("=") }
.mapNotNull { line ->
val values = line.split("=")
if (values.size == 2) {
values[0] to values[1]
} else {
null
}
}
.toMap()
}

private fun classifyDesktopEnvironment(
environment: Map<String, String>
): DesktopType {
val typeByXdgSession = classifyByXdgSessionTypeVariable(environment)
if (typeByXdgSession != null) {
return typeByXdgSession
}

val typeByDesktopSession = classifyByDesktopSessionVariable(environment)
if (typeByDesktopSession != null) {
return typeByDesktopSession
}

return classifyByOthers(environment)
}

private fun classifyByXdgSessionTypeVariable(
environment: Map<String, String>
): DesktopType? {
val xdgSessionType = environment[XDG_SESSION_TYPE] ?: return null

for (type in TYPES) {
if (xdgSessionType.contains(type.key, ignoreCase = true)) {
return type.value
}
}

return null
}

private fun classifyByDesktopSessionVariable(
environment: Map<String, String>
): DesktopType? {
val desktopSession = environment[DESKTOP_SESSION] ?: return null

for (type in TYPES) {
if (desktopSession.contains(type.key, ignoreCase = true)) {
return type.value
}
}

return null
}

private fun classifyByOthers(
environment: Map<String, String>
): DesktopType {
val isWayland = environment.any { (_, value) ->
value.contains(WAYLAND, ignoreCase = true)
}

return if (isWayland) {
DesktopType.WAYLAND
} else {
DesktopType.XORG
}
}

companion object {
const val COMMAND = "env"
private const val WAYLAND = "wayland"

private val TYPES = mapOf(
"wayland" to DesktopType.WAYLAND,
"x11" to DesktopType.XORG,
"xorg" to DesktopType.XORG
)

const val XDG_SESSION_TYPE = "XDG_SESSION_TYPE"
const val DESKTOP_SESSION = "DESKTOP_SESSION"
}
}
6 changes: 6 additions & 0 deletions src/main/java/com/github/ai/autokpass/model/DesktopType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.github.ai.autokpass.model

enum class DesktopType {
XORG,
WAYLAND
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import com.github.ai.autokpass.domain.autotype.AutotypeSequenceFactory
import com.github.ai.autokpass.domain.coroutine.Dispatchers
import com.github.ai.autokpass.domain.exception.AutokpassException
import com.github.ai.autokpass.domain.usecases.DetermineAutotypeExecutorTypeUseCase
import com.github.ai.autokpass.domain.usecases.DetermineDesktopUseCase
import com.github.ai.autokpass.domain.usecases.GetOSTypeUseCase
import com.github.ai.autokpass.domain.window.FocusedWindowProvider
import com.github.ai.autokpass.model.AutotypeExecutorType
import com.github.ai.autokpass.model.AutotypePattern
import com.github.ai.autokpass.model.AutotypeState
import com.github.ai.autokpass.model.DesktopType
import com.github.ai.autokpass.model.KeepassEntry
import com.github.ai.autokpass.model.OSType
import com.github.ai.autokpass.model.ParsedConfig
Expand All @@ -31,6 +33,7 @@ class AutotypeInteractor(
private val sequenceFactory: AutotypeSequenceFactory,
private val getOSTypeUseCase: GetOSTypeUseCase,
private val determineExecutorTypeUseCase: DetermineAutotypeExecutorTypeUseCase,
private val determineDesktopTypeUseCase: DetermineDesktopUseCase,
private val strings: StringResources
) {

Expand All @@ -42,9 +45,16 @@ class AutotypeInteractor(
return getOsTypeResult.asErrorOrThrow()
}

val getDesktopTypeResult = determineDesktopTypeUseCase.getDesktopType()
if (getDesktopTypeResult.isFailed()) {
return getDesktopTypeResult.asErrorOrThrow()
}

val osType = getOsTypeResult.getDataOrNull()
val desktopType = getDesktopTypeResult.getDataOrNull()

val isAbleToAwait = (osType == OSType.LINUX && autotypeType == null) ||
val isXorgEnvironment = (osType == OSType.LINUX && desktopType == DesktopType.XORG)
val isAbleToAwait = (isXorgEnvironment && autotypeType == null) ||
autotypeType == AutotypeExecutorType.XDOTOOL

return Result.Success(isAbleToAwait)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package com.github.ai.autokpass.domain.usecases

import com.github.ai.autokpass.TestData.EXCEPTION
import com.github.ai.autokpass.domain.usecases.DetermineDesktopUseCase.Companion.COMMAND
import com.github.ai.autokpass.domain.usecases.DetermineDesktopUseCase.Companion.DESKTOP_SESSION
import com.github.ai.autokpass.domain.usecases.DetermineDesktopUseCase.Companion.XDG_SESSION_TYPE
import com.github.ai.autokpass.model.DesktopType
import com.github.ai.autokpass.model.DesktopType.WAYLAND
import com.github.ai.autokpass.model.DesktopType.XORG
import com.github.ai.autokpass.model.Result
import com.github.ai.autokpass.presentation.process.MockProcessExecutor
import com.github.ai.autokpass.util.StringUtils.EMPTY
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test

class DetermineDesktopUseCaseTest {

@Test
fun `should return error if unable to execute env command`() {
// arrange
val processExecutor = MockProcessExecutor(
data = mapOf(
COMMAND to Result.Error(EXCEPTION)
)
)

// act
val result = DetermineDesktopUseCase(processExecutor).getDesktopType()

// assert
result shouldBe Result.Error(EXCEPTION)
}

@Test
fun `should classify by environment variables`() {
assertUseCase(
data = listOf(
"$XDG_SESSION_TYPE=wayland-session" to Result.Success(WAYLAND),
"$XDG_SESSION_TYPE=x11-session" to Result.Success(XORG),
"$XDG_SESSION_TYPE=xorg-session" to Result.Success(XORG),

"$DESKTOP_SESSION=wayland-session" to Result.Success(WAYLAND),
"$DESKTOP_SESSION=x11-session-x11" to Result.Success(XORG),
"$DESKTOP_SESSION=xorg-session" to Result.Success(XORG),

"ENVIRONMENT_VARIABLE=wayland-session" to Result.Success(WAYLAND)
)
)
}

@Test
fun `should return xorg if unable to classify by environment variables`() {
assertUseCase(
data = listOf(
"$XDG_SESSION_TYPE=invalid" to Result.Success(XORG),
"$DESKTOP_SESSION=invalid" to Result.Success(XORG)
)
)
}

@Test
fun `should return xorg type if nothing is specified`() {
assertUseCase(
data = listOf(EMPTY to Result.Success(XORG))
)
}

@Test
fun `should ignore invalid lines`() {
assertUseCase(
data = listOf(
"""
$XDG_SESSION_TYPE=x11=invalid
$DESKTOP_SESSION=wayland
""".trimIndent() to Result.Success(WAYLAND),

"""
$XDG_SESSION_TYPE=wayland=invalid
$DESKTOP_SESSION=x11
""".trimIndent() to Result.Success(XORG),

"""
WAYLAND
$DESKTOP_SESSION=x11
""".trimIndent() to Result.Success(XORG)
)
)
}

private fun assertUseCase(
data: List<Pair<String, Result<DesktopType>>>
) {
data.forEach { (input, expected) ->
val processExecutor = MockProcessExecutor(
data = mapOf(
COMMAND to Result.Success(input)
)
)

// act
val result = DetermineDesktopUseCase(
processExecutor = processExecutor
).getDesktopType()

// assert
result shouldBe expected
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.github.ai.autokpass.presentation.process

import com.github.ai.autokpass.model.Result

class MockProcessExecutor(
private val data: Map<String, Result<String>>
) : ProcessExecutor {

override fun execute(command: String): Result<String> {
return data[command]
?: throw IllegalArgumentException("Unable to find data for command: $command")
}

override fun executeWithBash(command: String): Result<String> {
throw NotImplementedError()
}

override fun execute(input: ByteArray, command: String): Result<String> {
throw NotImplementedError()
}
}

0 comments on commit a86a39c

Please sign in to comment.