1
- package com.github.siropkin.kursor.keyboardlayout
1
+ package com.github.siropkin.kursor.keyboard
2
2
3
3
import com.sun.jna.Platform
4
4
import com.sun.jna.platform.win32.User32
5
5
import com.sun.jna.platform.win32.WinDef
6
6
import com.sun.jna.platform.win32.WinDef.HKL
7
7
import java.awt.im.InputContext
8
- import java.io.BufferedReader
9
- import java.io.IOException
10
8
11
9
12
- class KeyboardLayout {
13
- private val unknown = " UNK"
14
- private var linuxDistribution: String = System .getenv(" DESKTOP_SESSION" )?.lowercase() ? : " "
15
- private var linuxDesktopGroup: String = System .getenv(" XDG_SESSION_TYPE" )?.lowercase() ? : " "
16
- private var linuxKeyboardLayoutsCache: List <String > = emptyList()
10
+ private const val UNKNOWN = " UNK"
17
11
18
- fun getLayoutInfo (): KeyboardLayoutInfo {
12
+ class Keyboard {
13
+ private var linuxConfig: LinuxConfig ? = null
14
+
15
+ fun getLayout (): KeyboardLayout {
16
+ if (Platform .isLinux() && linuxConfig == null ) {
17
+ linuxConfig = LinuxConfig ()
18
+ }
19
19
return when {
20
- Platform .isLinux() -> getLinuxLayoutInfo ()
21
- Platform .isMac() -> getMacLayoutInfo ()
22
- Platform .isWindows() -> getWindowsLayoutInfo ()
23
- else -> getUnknownLayoutInfo ()
20
+ Platform .isLinux() -> getLinuxLayout ()
21
+ Platform .isMac() -> getMacLayout ()
22
+ Platform .isWindows() -> getWindowsLayout ()
23
+ else -> getUnknownLayout ()
24
24
}
25
25
}
26
26
27
- private fun getUnknownLayoutInfo (): KeyboardLayoutInfo {
28
- return KeyboardLayoutInfo (unknown, unknown, unknown )
27
+ private fun getUnknownLayout (): KeyboardLayout {
28
+ return KeyboardLayout ( UNKNOWN , UNKNOWN , UNKNOWN )
29
29
}
30
30
31
- private fun getLinuxLayoutInfo (): KeyboardLayoutInfo {
31
+ private fun getLinuxLayout (): KeyboardLayout {
32
32
// InputContext.getInstance().locale is not working on Linux: it always returns "en_US"
33
33
// This is not the ideal solution because it involves executing a shell command to know the current keyboard layout
34
34
// which might affect the performance. And we have different commands for different Linux distributions.
35
35
// But it is the only solution I found that works on Linux.
36
36
// For Linux we know only keyboard layout and do not know keyboard language
37
+ val config = linuxConfig ? : return getUnknownLayout()
37
38
return when {
38
- linuxDistribution == " ubuntu" -> getUbuntuLayoutInfo ()
39
- linuxDesktopGroup == " wayland" -> getWaylandLayoutInfo ()
40
- else -> getOtherLinuxLayoutInfo ()
39
+ config.distribution == " ubuntu" -> getUbuntuLayout ()
40
+ config.desktopGroup == " wayland" -> getWaylandLayout ()
41
+ else -> getOtherLinuxLayout ()
41
42
}
42
43
}
43
44
44
- private fun getUbuntuLayoutInfo (): KeyboardLayoutInfo {
45
+ private fun getUbuntuLayout (): KeyboardLayout {
45
46
// Output example: [('xkb', 'us'), ('xkb', 'ru'), ('xkb', 'ca+eng')]
46
- val split = executeNativeCommand(arrayOf(" gsettings" , " get" , " org.gnome.desktop.input-sources" , " mru-sources" ))
47
+ val split = Utils . executeNativeCommand(arrayOf(" gsettings" , " get" , " org.gnome.desktop.input-sources" , " mru-sources" ))
47
48
.substringAfter(" ('xkb', '" )
48
49
.substringBefore(" ')" )
49
50
.split(" +" )
50
51
val language = if (split.size > 1 ) split[1 ] else " "
51
52
val country = split[0 ]
52
- return KeyboardLayoutInfo (language, country, " " )
53
+ return KeyboardLayout (language, country, " " )
53
54
}
54
55
55
- private fun getWaylandLayoutInfo (): KeyboardLayoutInfo {
56
+ private fun getWaylandLayout (): KeyboardLayout {
56
57
// FIXME: Other Linux distribution commands not working "Wayland",
57
58
// see: https://github.com/siropkin/kursor/issues/3
58
- return getUnknownLayoutInfo ()
59
+ return getUnknownLayout ()
59
60
}
60
61
61
- private fun getOtherLinuxLayoutInfo (): KeyboardLayoutInfo {
62
- if (linuxKeyboardLayoutsCache.isEmpty()) {
62
+ private fun getOtherLinuxLayout (): KeyboardLayout {
63
+ val config = linuxConfig ? : return getUnknownLayout()
64
+ if (config.availableKeyboardLayouts.isEmpty()) {
63
65
// Output example: rules: evdev
64
66
// model: pc105
65
67
// layout: us
66
68
// options: grp:win_space_toggle,terminate:ctrl_alt_bksp
67
- linuxKeyboardLayoutsCache = executeNativeCommand(arrayOf(" setxkbmap" , " -query" ))
69
+ config.availableKeyboardLayouts = Utils . executeNativeCommand(arrayOf(" setxkbmap" , " -query" ))
68
70
.substringAfter(" layout:" )
69
71
.substringBefore(" \n " )
70
72
.trim()
@@ -98,46 +100,45 @@ class KeyboardLayout {
98
100
// Standby: 0 Suspend: 0 Off: 0
99
101
// DPMS is Enabled
100
102
// Monitor is On
101
- val linuxCurrentKeyboardLayoutIndex = executeNativeCommand(arrayOf(" xset" , " -q" ))
103
+ val linuxCurrentKeyboardLayoutIndex = Utils . executeNativeCommand(arrayOf(" xset" , " -q" ))
102
104
.substringAfter(" LED mask:" )
103
105
.substringBefore(" \n " )
104
106
.trim()
105
107
.substring(4 , 5 )
106
108
.toInt(16 )
107
109
108
110
// Additional check to avoid out-of-bounds exception
109
- if (linuxCurrentKeyboardLayoutIndex >= linuxKeyboardLayoutsCache .size) {
110
- return getUnknownLayoutInfo ()
111
+ if (linuxCurrentKeyboardLayoutIndex >= config.availableKeyboardLayouts .size) {
112
+ return getUnknownLayout ()
111
113
}
112
114
113
115
// This is a bad solution because it returns 0 if it's a default layout and 1 in other cases,
114
116
// and if user has more than two layouts, we do not know which one is really on
115
- if (linuxKeyboardLayoutsCache .size > 2 && linuxCurrentKeyboardLayoutIndex > 0 ) {
116
- return getUnknownLayoutInfo ()
117
+ if (config.availableKeyboardLayouts .size > 2 && linuxCurrentKeyboardLayoutIndex > 0 ) {
118
+ return getUnknownLayout ()
117
119
}
118
120
119
- val country = linuxKeyboardLayoutsCache [linuxCurrentKeyboardLayoutIndex]
120
- return KeyboardLayoutInfo (" " , country, " " )
121
+ val country = config.availableKeyboardLayouts [linuxCurrentKeyboardLayoutIndex]
122
+ return KeyboardLayout (" " , country, " " )
121
123
}
122
124
123
- private fun getMacLayoutInfo (): KeyboardLayoutInfo {
125
+ private fun getMacLayout (): KeyboardLayout {
124
126
val locale = InputContext .getInstance().locale
125
- // Variant example for US: UserDefined_252
126
- val variant = MacKeyboardVariants [locale.variant] ? : " "
127
- return KeyboardLayoutInfo (locale.language, locale.country, variant)
127
+ val localeVariant = locale.variant.removePrefix(" UserDefined_" )
128
+ val variant = MacStandardKeyboardVariants [localeVariant]
129
+ ? : MacSogouPinyinVariants [localeVariant]
130
+ ? : MacRimeSquirrelVariants [localeVariant]
131
+ ? : " "
132
+ return KeyboardLayout (locale.language, locale.country, variant)
128
133
}
129
134
130
- private fun getWindowsLayoutInfo (): KeyboardLayoutInfo {
135
+ private fun getWindowsLayout (): KeyboardLayout {
131
136
val locale = InputContext .getInstance().locale
132
137
// Standard locale object does not return correct info in case user set different keyboard inputs for one language
133
138
// see: https://github.com/siropkin/kursor/issues/4
134
139
val user32 = User32 .INSTANCE
135
- val fgWindow: WinDef .HWND ? = user32.GetForegroundWindow () // Get the handle of the foreground window
136
-
137
- if (fgWindow == null ) {
138
- return KeyboardLayoutInfo (locale.language, locale.country, " " )
139
- }
140
-
140
+ val fgWindow: WinDef .HWND = user32.GetForegroundWindow ()
141
+ ? : return KeyboardLayout (locale.language, locale.country, " " ) // Get the handle of the foreground window
141
142
val threadId = user32.GetWindowThreadProcessId (fgWindow, null ) // Get the thread ID of the foreground window
142
143
val hkl: HKL = user32.GetKeyboardLayout (threadId) // Get the keyboard layout for the thread
143
144
// FIXME: It should be a better way how to convert pointer to string
@@ -151,18 +152,7 @@ class KeyboardLayout {
151
152
else -> layoutId.substring(2 ).padStart(8 , ' 0' )
152
153
}
153
154
val variant = WindowsKeyboardVariants [layoutId.uppercase()] ? : " "
154
- return KeyboardLayoutInfo (locale.language, locale.country, variant)
155
- }
156
-
157
- private fun executeNativeCommand (command : Array <String >): String {
158
- return try {
159
- val process = Runtime .getRuntime().exec(command)
160
- process.waitFor()
161
- process.inputStream.bufferedReader().use(BufferedReader ::readText)
162
- } catch (e: IOException ) {
163
- e.printStackTrace()
164
- " "
165
- }
155
+ return KeyboardLayout (locale.language, locale.country, variant)
166
156
}
167
157
}
168
158
0 commit comments