Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit

Permalink
Implement Windows JVM implementation for NativeKeyboardHandler with t…
Browse files Browse the repository at this point in the history
…ests.

Add runtime dependency to JUnit5 which was failing the tests.
  • Loading branch information
Animeshz committed Mar 28, 2021
1 parent 9db25ae commit 697d7a1
Show file tree
Hide file tree
Showing 16 changed files with 311 additions and 88 deletions.
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Following are the future plans for the project:
- [X] Implement way to cross compile the C/C++ library from any OS to any OS and then package it up in the resulting
Jar. Done with PR [#4](https://github.com/Animeshz/keyboard-mouse-kt/pull/4)
on [jvm branch](https://github.com/Animeshz/keyboard-mouse-kt/tree/jvm)
- Implement JNI each for different platforms. I've considered it to do via C++ instead of reusing Kotlin/Native because
- [X] (Complete for Windows x64 currently) Implement JNI each for different platforms. I've considered it to do via C++ instead of reusing Kotlin/Native because
it will result in low performance and maybe huge sizes (if K/N becomes stable and performance wise equivalent we can
directly reuse the sources we've written).
- Add Linux Device (`/dev/uinput` | `/dev/input/xxx`) based implementation of interaction of Keyboard/Mouse as a
Expand All @@ -54,6 +54,6 @@ Following are the future plans for the project:
To build and publish to mavenLocal:
`$ ./gradlew build publishToMavenLocal`

The only dependency is to install Docker when building for JVM due to cross-compilation requirement of JNI native libs to be able to pack the full Jar from any platform that is supported cross-platform.
The only requirement is to install Docker when building for JVM due to cross-compilation requirement of JNI native libs to be able to pack the full Jar from any platform that is supported cross-platform.

[1]: https://github.com/Animeshz/keyboard-mouse-kt/issues/1
38 changes: 18 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,31 @@ __KeyboardMouse.kt is still in an experimental stage, as such we can't guarantee

## What is KeyboardMouse.kt

KeyboardMouse.kt is a coroutine-based cross-platform implementation of Global Keyboard and Mouse events, written 100% in Kotlin.
KeyboardMouse.kt is a coroutine-based cross-platform implementation of Global Keyboard and Mouse interactions, written 100% in Kotlin.

We aim to provide high-level as well as high-performant low-level access to such APIs. Sometimes you have to do some unconventional things, and we want to allow you to do those in concise, safe and supported way.

## Status of KeyboardMouse.kt

- [ ] Keyboard
- [X] Windows <sup>1</sup>
- [X] Linux <sup>1</sup>
- [X] Windows
- [X] x86_64 (64 bit)
- [ ] x86 (32 bit)
- [X] Linux
- [X] x86_64 (64 bit)
- [ ] x86 (32 bit)
- [ ] MacOS
- [ ] JVM <sup>2</sup>
- [ ] JVM
- [X] Windows x86_64
- [ ] Windows x86
- [ ] Linux x86_64
- [ ] Linux x86
- [ ] Mouse
- [ ] Windows
- [ ] Linux
- [ ] MacOS
- [ ] JVM

<sub>1. Tests are remaining (tests are on hold due to MockK does not support K/N).</sub><br>
<sub>2. Setup of cross compilation which was the main thing in automated build has been done, code should be written in the same way as we do in the Kotlin/Native part, which is not so far away :) See [CONTRIBUTING.md](https://github.com/Animeshz/keyboard-mouse-kt/blob/master/CONTRIBUTING.md) for the roadmap</sub>


## Installation

Expand All @@ -53,30 +58,23 @@ repositories {

kotlin {
// Your targets
jvm()
mingwX64 {
binaries { executable { entryPoint = "main" } }
}
linuxX64 {
binaries { executable { entryPoint = "main" } }
}

// Dependency to the library
sourceSets {
// Either in common:
val commonMain by getting {
dependencies {
implementation(kotlin("stdlib-common"))
implementation("com.github.animeshz:keyboard-kt:<version>")
implementation("com.github.animeshz:mouse-kt:<version>")
}
}

// Or configuring per platform:
val mingwX64Main by getting {
dependencies {
implementation("com.github.animeshz:keyboard-kt-mingwX64:<version>")
implementation("com.github.animeshz:mouse-kt-mingwX64:<version>")
}
}
}
}
```
Expand All @@ -90,9 +88,9 @@ Low Level API depends on [NativeKeyboardHandler][1] that can be obtained via [na
- Listening to events using Flow.
```kotlin
handler.events
.filter { it.state == KeyState.KeyDown }
.map { it.key }
.collect { println(it) }
.filter { it.state == KeyState.KeyDown }
.map { it.key }
.collect { println(it) }
```
- Sending a [Key][3] event.
```kotlin
Expand Down Expand Up @@ -151,6 +149,6 @@ High Level API depends on [Keyboard][4] which is a wrapper around the [NativeKey

[5]: https://github.com/Animeshz/keyboard-mouse-kt/blob/master/keyboard/src/commonMain/kotlin/com/github/animeshz/keyboard/entity/KeySet.kt

[6]: https://github.com/Animeshz/keyboard-mouse-kt/blob/master/keyboard/src/commonMain/kotlin/com/github/animeshz/keyboard/Keyboard.kt#L31
[6]: https://github.com/Animeshz/keyboard-mouse-kt/blob/master/keyboard/src/commonMain/kotlin/com/github/animeshz/keyboard/Keyboard.kt#L33

[7]: https://github.com/Animeshz/keyboard-mouse-kt/blob/master/keyboard/src/commonMain/kotlin/com/github/animeshz/keyboard/events/KeyEvent.kt
19 changes: 12 additions & 7 deletions keyboard/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ fun KotlinMultiplatformExtension.configureJvm() {
val jvmTest by sourceSets.getting {
dependsOn(jvmMain)
dependencies {
implementation("io.kotest:kotest-runner-junit5:4.3.2")
implementation(kotlin("test-junit5"))
runtimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.0")
implementation("io.kotest:kotest-assertions-core:4.3.2")
}
}
Expand All @@ -43,7 +44,6 @@ fun KotlinMultiplatformExtension.configureJvm() {
jvmMain.resources.srcDir("build/jni")
jvmTest.resources.srcDir("build/jni")

// Generating Jni headers
val generateJniHeaders by tasks.creating {
group = "build"
dependsOn(tasks.getByName("compileKotlinJvm"))
Expand All @@ -66,14 +66,14 @@ fun KotlinMultiplatformExtension.configureJvm() {

val output = ByteArrayOutputStream().use {
project.exec {
commandLine(javap, "-cp", buildDir.absolutePath, file.absolutePath)
commandLine(javap, "-private", "-cp", buildDir.absolutePath, file.absolutePath)
standardOutput = it
}.assertNormalExitValue()
it.toString()
}

val (packageName, className, methodInfo) =
"""public \w*\s*class (.+)\.(\w+) (?:implements.*)\{\R([^\}]*)\}""".toRegex().find(output)?.destructured ?: return@forEach
"""public \w*\s*class (.+)\.(\w+) (?:implements|extends).*\{\R([^\}]*)\}""".toRegex().find(output)?.destructured ?: return@forEach
val nativeMethods =
""".*\bnative\b.*""".toRegex().findAll(methodInfo).mapNotNull { it.groups }.flatMap { it.asSequence().mapNotNull { group -> group?.value } }.toList()
if (nativeMethods.isEmpty()) return@forEach
Expand All @@ -86,14 +86,18 @@ fun KotlinMultiplatformExtension.configureJvm() {
else {
val updatedMethod = StringBuilder(method).apply {
var count = 0
for (i in indices) if (this[i] == ',' || this[i] == ')') insert(i, " arg${count++}")
var i = 0
while (i < length) {
if (this[i] == ',' || this[i] == ')') insert(i, " arg${count++}".also { i += it.length + 1 })
else i++
}
}
appendln(updatedMethod)
}
}
appendln("}")
}
val outputFile = tmpDir.resolve(packageName.replace(".", "/")).apply { mkdirs() }.resolve("$className.java").apply { createNewFile() }
val outputFile = tmpDir.resolve(packageName.replace(".", "/")).apply { mkdirs() }.resolve("$className.java").apply { delete() }.apply { createNewFile() }
outputFile.writeText(source)

project.exec {
Expand All @@ -104,7 +108,6 @@ fun KotlinMultiplatformExtension.configureJvm() {
}

// For building shared libraries out of C/C++ sources

val compileJni by tasks.creating {
group = "build"
dependsOn(generateJniHeaders)
Expand Down Expand Up @@ -168,6 +171,8 @@ fun KotlinMultiplatformExtension.configureJvm() {
}
}
var output = work()

// Fix non-daemon docker on Docker for Windows
val nonDaemonError = "docker: error during connect: This error may indicate that the docker daemon is not running."
if (Os.isFamily(Os.FAMILY_WINDOWS) && output.startsWith(nonDaemonError)) {
project.exec { commandLine("C:\\Program Files\\Docker\\Docker\\DockerCli.exe", "-SwitchDaemon") }.assertNormalExitValue()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
Expand Down Expand Up @@ -49,6 +50,7 @@ public typealias KeyPressSequence = List<Pair<Duration, KeyEvent>>
*/
@Suppress("unused", "MemberVisibilityCanBePrivate")
@ExperimentalKeyIO
@ExperimentalCoroutinesApi
public class Keyboard(
context: CoroutineContext = Dispatchers.Default
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,29 @@
package com.github.animeshz.keyboard

/**
* Tests can be tried out after enabling granular source-set metadata in gradle.properties
import com.github.animeshz.keyboard.entity.Key
import com.github.animeshz.keyboard.events.KeyEvent
import com.github.animeshz.keyboard.events.KeyState
import io.kotest.matchers.comparables.shouldNotBeEqualComparingTo
import kotlin.test.Test
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.runBlocking
import kotlin.test.assertNotEquals

/**
* This is not really a Unit Test (since mocking is not available in Native),
* but rather a real-time test (in other words you have to interact :p).
*/
@ExperimentalKeyIO
class NativeKeyboardHandlerTest {
@Test
fun `get state of Key`() = runBlocking {
val handler = nativeKbHandlerForPlatform()
@Test
fun `Caps lock key should be toggled when KeyDown event is triggered`() {
val handler = nativeKbHandlerForPlatform()

delay(3000) // To have a delay to check if KeyDown comes :P
println("State of Key A: ${handler.getKeyState(Key.A)}")
}
val initialState = handler.isCapsLockOn()

@Test
fun `get state of Caps Lock`() = runBlocking {
val handler = nativeKbHandlerForPlatform()
handler.sendEvent(KeyEvent(Key.CapsLock, KeyState.KeyDown))
handler.sendEvent(KeyEvent(Key.CapsLock, KeyState.KeyUp))

println("Toggle state of CapsLock: ${if (handler.isCapsLockOn()) "On" else "Off"}")
}
val finalState = handler.isCapsLockOn()

@Test
fun `listening to events`() = runBlocking {
val handler = nativeKbHandlerForPlatform()
// Set the state back to initialState
handler.sendEvent(KeyEvent(Key.CapsLock, KeyState.KeyDown))
handler.sendEvent(KeyEvent(Key.CapsLock, KeyState.KeyUp))

println("Listening for first 5 events")
handler.events.take(5).collect { println(it) }
}
finalState shouldNotBeEqualComparingTo initialState
}
}
*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.github.animeshz.keyboard

/**
* Tests can be tried out after enabling granular source-set metadata in gradle.properties
import com.github.animeshz.keyboard.entity.Key
import kotlin.test.Test
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.runBlocking
/**
* This is not really a Unit Test (since mocking is not available in Native),
* but rather a real-time test (in other words you have to interact :p).
*/
@ExperimentalKeyIO
class NativeKeyboardHandlerTest {
@Test
fun `get state of Key`() = runBlocking {
val handler = nativeKbHandlerForPlatform()
delay(3000) // To have a delay to check if KeyDown comes :P
println("State of Key A: ${handler.getKeyState(Key.A)}")
}
@Test
fun `get state of Caps Lock`() = runBlocking {
val handler = nativeKbHandlerForPlatform()
println("Toggle state of CapsLock: ${if (handler.isCapsLockOn()) "On" else "Off"}")
}
@Test
fun `listening to events`() = runBlocking {
val handler = nativeKbHandlerForPlatform()
println("Listening for first 5 events")
handler.events.take(5).collect { println(it) }
}
}
*/

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions keyboard/src/jvmMain/jni/linux-x64/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
cmake_minimum_required(VERSION 3.10)
project(KeyboardKtx64)

set(CMAKE_CXX_STANDARD 11)

include_directories(KeyboardKtx64 PRIVATE $ENV{JNI_HEADERS_DIR})
include_directories(KeyboardKtx64 PRIVATE "../../generated/jni")

Expand Down
2 changes: 2 additions & 0 deletions keyboard/src/jvmMain/jni/windows-x64/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
cmake_minimum_required(VERSION 3.10)
project(KeyboardKtx64)

set(CMAKE_CXX_STANDARD 11)

include_directories(KeyboardKtx64 PRIVATE $ENV{JNI_HEADERS_DIR})
include_directories(KeyboardKtx64 PRIVATE "../../generated/jni")

Expand Down
Loading

0 comments on commit 697d7a1

Please sign in to comment.