Skip to content

Commit

Permalink
Use JNA to query window title instead
Browse files Browse the repository at this point in the history
  • Loading branch information
VixidDev committed Jul 2, 2024
1 parent 0c64a9e commit d807983
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 37 deletions.
2 changes: 1 addition & 1 deletion src/main/java/dev/vixid/vsm/mixin/MixinKeyboard.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package dev.vixid.vsm.mixin;

import dev.vixid.vsm.events.KeyPress;
import dev.vixid.vsm.spotify.ControlUtils;
import dev.vixid.vsm.features.spotify.ControlUtils;
import net.minecraft.client.Keyboard;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
Expand Down
9 changes: 1 addition & 8 deletions src/main/kotlin/dev/vixid/vsm/VSM.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,9 @@ import dev.vixid.vsm.config.core.VSMGsonMapper
import dev.vixid.vsm.config.core.annotations.ConfigEditorKeybind
import dev.vixid.vsm.config.core.gui.GuiOptionEditorKeybind
import dev.vixid.vsm.overlays.OverlayPositions
import dev.vixid.vsm.spotify.SpotifyDisplay
import dev.vixid.vsm.features.spotify.SpotifyDisplay
import io.github.notenoughupdates.moulconfig.managed.ManagedConfig
import java.io.File
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import net.fabricmc.api.ClientModInitializer
import org.slf4j.Logger
import org.slf4j.LoggerFactory
Expand All @@ -31,9 +27,6 @@ object VSM : ClientModInitializer {
}
}

private val globalJob = Job()
val coroutineScope = CoroutineScope(CoroutineName("VSM") + SupervisorJob(globalJob))

override fun onInitializeClient() {
GlobalScreen.registerNativeHook()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package dev.vixid.vsm.spotify
package dev.vixid.vsm.features.spotify

import com.github.kwhat.jnativehook.GlobalScreen
import com.github.kwhat.jnativehook.keyboard.NativeKeyEvent
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package dev.vixid.vsm.spotify
package dev.vixid.vsm.features.spotify

import dev.vixid.vsm.VSM
import dev.vixid.vsm.config.SpotifyConfig
Expand All @@ -7,10 +7,8 @@ import dev.vixid.vsm.events.MousePress
import dev.vixid.vsm.overlays.Overlay
import dev.vixid.vsm.overlays.OverlayPositions
import dev.vixid.vsm.utils.ChatUtils
import dev.vixid.vsm.utils.JNAHelper
import dev.vixid.vsm.utils.RenderUtils.drawTextWithShadow
import java.io.BufferedReader
import java.io.InputStreamReader
import kotlinx.coroutines.launch
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawContext
Expand Down Expand Up @@ -42,9 +40,18 @@ object SpotifyDisplay : Overlay() {
totalTicks++

if (totalTicks % 20 == 0) {
VSM.coroutineScope.launch {
songName = getSongFromSpotifyProcess()
var windowTitle = JNAHelper.getProcessWindowTitle("Spotify.exe")

if (windowTitle == "Spotify") {
windowTitle = songName
} else if (windowTitle.isNotEmpty()) {
windowTitle = "§a${windowTitle}".replace(" - ", " §f-§b ")

if (songName != windowTitle) {
ChatUtils.chat("§bVSM §f> $windowTitle")
}
}
songName = windowTitle
}
}

Expand All @@ -67,26 +74,5 @@ object SpotifyDisplay : Overlay() {
}
}

private fun getSongFromSpotifyProcess() : String {
return try {
val process = Runtime.getRuntime().exec("powershell.exe (ps Spotify | ? MainWindowTitle | select MainWindowTitle).MainWindowTitle")
val reader = BufferedReader(InputStreamReader(process.inputStream))
var line = reader.readLine()
reader.close()
if (line == "Spotify") {
line = songName
} else if (line != null) {
line = "§a${line}".replace(" - ", " §f-§b ")
if (!line.equals(songName)) {
ChatUtils.chat("§bVSM §f> $line")
}
}
line ?: "§cCannot detect song name!"
} catch (error: Exception) {
error.printStackTrace()
"§cCannot detect song name!"
}
}

private fun isEnabled(client: MinecraftClient) = client.world != null && config.enabled
}
120 changes: 120 additions & 0 deletions src/main/kotlin/dev/vixid/vsm/utils/JNAHelper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
@file:Suppress("FunctionName", "LocalVariableName")

package dev.vixid.vsm.utils

import com.sun.jna.Native
import com.sun.jna.Platform
import com.sun.jna.Pointer
import com.sun.jna.platform.win32.WinDef
import com.sun.jna.platform.win32.WinNT
import com.sun.jna.platform.win32.WinUser
import com.sun.jna.ptr.IntByReference
import com.sun.jna.win32.StdCallLibrary
import com.sun.jna.win32.W32APIOptions

object JNAHelper {

private val INSTANCE: NativeJNAHelper by lazy {
if (Platform.isWindows()) {
WindowsJNAHelper
} else if (Platform.isX11()) {
X11JNAHelper
} else if (Platform.isMac()) {
MacJNAHelper
} else {
val os = System.getProperty("os.name")
throw UnsupportedOperationException("No support for $os")
}
}

private object WindowsJNAHelper : NativeJNAHelper() {
interface User32 : StdCallLibrary {
companion object {
val INSTANCE = Native.load("user32", User32::class.java, W32APIOptions.DEFAULT_OPTIONS) as User32
}

fun EnumWindows(lpEnumFunc: WinUser.WNDENUMPROC?, arg: Pointer?): Boolean
fun GetWindowThreadProcessId(hWnd: WinDef.HWND?, lpdwProcessId: IntByReference?): Int
fun GetWindowTextW(hwnd: WinDef.HWND?, lpString: CharArray?, nMaxCount: Int): Int
}

interface Kernel32 : StdCallLibrary {
companion object {
val INSTANCE = Native.load("Kernel32", Kernel32::class.java, W32APIOptions.DEFAULT_OPTIONS) as Kernel32
}

fun OpenProcess(fdwAccess: Int, fInherit: Boolean, IDProcess: Int): WinNT.HANDLE?
}

interface Psapi : StdCallLibrary {
companion object {
val INSTANCE = Native.load("Psapi", Psapi::class.java, W32APIOptions.DEFAULT_OPTIONS) as Psapi
}

fun GetModuleBaseNameW(hProcess: Pointer, hModule: Pointer?, lpBaseName: CharArray, nSize: Int): WinDef.DWORD
}

private val ignoredStrings = arrayOf("Default IME", "MSCTFIME UI", "GDI+ Window")

override fun getProcessWindowTitle(process: String): String {
val PROCESS_VM_READ = 0x0010
val PROCESS_QUERY_INFORMATION = 0x0400

val user32 = User32.INSTANCE
val kernel32 = Kernel32.INSTANCE
val psapi = Psapi.INSTANCE

var title = ""

user32.EnumWindows({ hwnd: WinDef.HWND, _: Pointer? ->
val pid = IntByReference()
user32.GetWindowThreadProcessId(hwnd, pid)
val processHandle = kernel32.OpenProcess(PROCESS_VM_READ or PROCESS_QUERY_INFORMATION, false, pid.value)

if (processHandle != null) {
val moduleNameBuf = CharArray(512)
psapi.GetModuleBaseNameW(processHandle.pointer, Pointer.NULL, moduleNameBuf, 512)
val moduleName = Native.toString(moduleNameBuf)

if (moduleName.equals(process)) {
val titleBuf = CharArray(512)
user32.GetWindowTextW(hwnd, titleBuf, 512)
val windowTitle = Native.toString(titleBuf)

if (windowTitle.isNotEmpty() && !ignore(windowTitle)) {
title = windowTitle
}
}
}
true
}, null)

return title
}

private fun ignore(title: String): Boolean {
for (string in ignoredStrings) {
if (title.contains(string)) return true
}
return false
}
}

private object X11JNAHelper : NativeJNAHelper() {
override fun getProcessWindowTitle(process: String): String {
throw UnsupportedOperationException("This function has not been implemented for this platform yet!")
}
}

private object MacJNAHelper : NativeJNAHelper() {
override fun getProcessWindowTitle(process: String): String {
throw UnsupportedOperationException("This function has not been implemented for this platform yet!")
}
}

private abstract class NativeJNAHelper {
abstract fun getProcessWindowTitle(process: String): String
}

fun getProcessWindowTitle(process: String): String = INSTANCE.getProcessWindowTitle(process)
}

0 comments on commit d807983

Please sign in to comment.