-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathkblayouttool.py
276 lines (214 loc) · 8.45 KB
/
kblayouttool.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
#
# BSD 2-Clause License
#
# Copyright (c) 2020, Dane Finlay
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
"""
kblayouttool
This is a utility program for working with Windows keyboard layouts. It can function
as a sort of command-line replacement for the Windows language bar.
"""
# pylint: disable=E0401
# This file imports Win32-only modules.
import argparse
import logging
import time
import win32api
import win32con
import win32gui
import win32process
#------------------------------------------------------------------------------------
# Win32 library functions.
def post_lang_change_request_message(hwnd, wparam, lparam):
""" Post a WM_INPUTLANGCHANGEREQUEST message with the specified parameters. """
win32gui.PostMessage(hwnd, win32con.WM_INPUTLANGCHANGEREQUEST, wparam, lparam)
def get_current_layout():
"""
Get the foreground window's current keyboard layout.
:rtype: int
:returns: current keyboard layout (HKL DWORD)
"""
thread_id = win32process.GetWindowThreadProcessId(
win32gui.GetForegroundWindow()
)[0]
# Return the HKL as a 32-bit unsigned integer.
return win32api.GetKeyboardLayout(thread_id) & 0xffffffff
def get_all_layouts():
"""
Get a list of all loaded keyboard layouts.
:rtype: list
:returns: keyboard layouts
"""
# Return each HKL as a 32-bit unsigned integer.
return [hkl & 0xffffffff for hkl in win32api.GetKeyboardLayoutList()]
def set_layout(wparam, lparam, broadcast):
"""
Set the current keyboard layout by posting a WM_INPUTLANGCHANGEREQUEST message.
:param wparam: WM_INPUTLANGCHANGEREQUEST message WPARAM value
:type wparam: int
:param lparam: WM_INPUTLANGCHANGEREQUEST message LPARAM value
:type lparam: int
:param broadcast: whether to post the request message to all windows
:type broadcast: bool
"""
# Post the request message to all windows, i.e. broadcast, if specified.
# Otherwise, only post the request message to the foreground window.
if broadcast:
hwnd = win32con.HWND_BROADCAST
else:
hwnd = win32gui.GetForegroundWindow()
# Post the request message.
post_lang_change_request_message(hwnd, wparam, lparam)
#------------------------------------------------------------------------------------
# CLI functions.
def _cli_get_layout(args):
# Delay getting the keyboard layout if a non-zero delay value was specified.
delay = args.delay
if delay:
time.sleep(delay)
# Get the current HKL and print it to stdout in hexadecimal format.
hkl = get_current_layout()
print("0x%08x" % hkl)
# Return the success of this command.
return 0
def _set_layout(args, wparam, lparam):
# Delay setting the keyboard layout if a non-zero delay value was specified.
delay = args.delay
if delay:
time.sleep(delay)
# Set the keyboard layout as specified.
set_layout(wparam, lparam, args.all_windows)
# Return the success of this command.
return 0
def _cli_set_layout(args):
return _set_layout(args, 0, args.hkl)
def _cli_prev_layout(args):
inputlangchange_backward = 0x0004
return _set_layout(args, inputlangchange_backward, 0)
def _cli_next_layout(args):
inputlangchange_forward = 0x0002
return _set_layout(args, inputlangchange_forward, 0)
def _cli_list_layouts(args):
# pylint: disable=W0613
# Get and print each HKL to stdout in hexadecimal format.
for hkl in get_all_layouts():
print("0x%08x" % hkl)
return 0
#------------------------------------------------------------------------------------
# argparse functions.
def _hexadecimal_or_int(string):
try:
return int(string)
except ValueError:
return int(string, 16)
def _build_argument(*args, **kwargs):
return args, kwargs
def _add_arguments(parser, *arguments):
for (args, kwargs) in arguments:
parser.add_argument(*args, **kwargs)
def _get_cli_arguments():
# Use argparse to parse command-line arguments.
parser = argparse.ArgumentParser(
description="Utility command-line program for working with Windows keyboard "
"layouts."
)
subparsers = parser.add_subparsers(dest='command')
subparsers.required = True
# Build commonly used arguments.
delay_argument = _build_argument(
"-d", "--delay", default=0, type=float,
help="Time in seconds to delay before getting/setting the keyboard "
"layout."
)
all_windows_argument = _build_argument(
"-a", "--all-windows", default=False, action="store_true",
help="Change the keyboard layout for all windows instead of only the "
"current foreground window. This may be useful on OS versions below "
"Windows 8."
)
# Create the parser for the "get-layout" command.
parser_get_layout = subparsers.add_parser(
"get-layout",
help="Print the current keyboard layout (HKL) of the foreground "
"window. This value is used as an argument for other commands."
)
_add_arguments(parser_get_layout, delay_argument)
# Create the parser for the "set-layout" command.
parser_set_layout = subparsers.add_parser(
"set-layout",
help="Set the current keyboard layout."
)
hkl_argument = _build_argument(
"hkl", type=_hexadecimal_or_int,
help="An input locale identifier (HKL) corresponding to the keyboard layout "
"to set."
)
_add_arguments(parser_set_layout, hkl_argument, all_windows_argument,
delay_argument)
# Create the parser for the "prev-layout" command.
parser_prev_layout = subparsers.add_parser(
"prev-layout",
help="Cycle backward one keyboard layout."
)
_add_arguments(parser_prev_layout, all_windows_argument,
delay_argument)
# Create the parser for the "next-layout" command.
parser_next_layout = subparsers.add_parser(
"next-layout",
help="Cycle forward one keyboard layout."
)
_add_arguments(parser_next_layout, all_windows_argument,
delay_argument)
# Create the parser for the "list-layouts" command.
subparsers.add_parser(
"list-layouts",
help="List each input locale identifier (HKL) corresponding to the current "
"set of input locales in the system. These identifiers can be used with the "
"'set-layout' command."
)
# Parse and return the arguments.
return parser.parse_args()
#------------------------------------------------------------------------------------
# Main function.
def _main():
# Set up logging.
logging.basicConfig(format="%(levelname)s: %(message)s")
log = logging.getLogger()
# Parse arguments.
args = _get_cli_arguments()
def not_implemented(_):
log.error("Command '%s' is not implemented.", args.command)
return 1
# Call the relevant CLI function and exit using the result.
command_map = {
"get-layout": _cli_get_layout,
"set-layout": _cli_set_layout,
"prev-layout": _cli_prev_layout,
"next-layout": _cli_next_layout,
"list-layouts": _cli_list_layouts,
}
func = command_map.get(args.command, not_implemented)
return_code = func(args)
exit(return_code)
if __name__ == '__main__':
_main()