-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlayout_switcher.py
More file actions
194 lines (155 loc) · 7.21 KB
/
layout_switcher.py
File metadata and controls
194 lines (155 loc) · 7.21 KB
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
import ctypes
import ctypes.util
from ctypes import byref, c_void_p
carbon_path = '/System/Library/Frameworks/Carbon.framework/Versions/Current/Carbon'
carbon = ctypes.CDLL(carbon_path)
cf_path = '/System/Library/Frameworks/CoreFoundation.framework/Versions/Current/CoreFoundation'
cf = ctypes.CDLL(cf_path)
CFTypeRef = ctypes.c_void_p
CFStringRef = ctypes.c_void_p
CFArrayRef = ctypes.c_void_p
CFDictionaryRef = ctypes.c_void_p
TISInputSourceRef = ctypes.c_void_p
OSStatus = ctypes.c_int
Boolean = ctypes.c_bool
cf.CFArrayGetCount.restype = ctypes.c_long
cf.CFArrayGetCount.argtypes = [CFArrayRef]
cf.CFArrayGetValueAtIndex.restype = ctypes.c_void_p
cf.CFArrayGetValueAtIndex.argtypes = [CFArrayRef, ctypes.c_long]
cf.CFStringGetCString.restype = Boolean
cf.CFStringGetCString.argtypes = [CFStringRef, ctypes.c_char_p, ctypes.c_long, ctypes.c_uint32]
cf.CFStringCreateWithCString.restype = CFStringRef
cf.CFStringCreateWithCString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint32]
carbon.TISCreateInputSourceList.restype = CFArrayRef
carbon.TISCreateInputSourceList.argtypes = [CFDictionaryRef, Boolean]
carbon.TISSelectInputSource.restype = OSStatus
carbon.TISSelectInputSource.argtypes = [TISInputSourceRef]
carbon.TISGetInputSourceProperty.restype = ctypes.c_void_p
carbon.TISGetInputSourceProperty.argtypes = [TISInputSourceRef, CFStringRef]
# UCKeyTranslate related functions
# For UCKeyTranslate, we need the keyboard layout ref and state.
# First, get current keyboard layout reference
carbon.TISCopyCurrentKeyboardInputSource.restype = TISInputSourceRef
carbon.TISCopyCurrentKeyboardInputSource.argtypes = []
carbon.TISGetInputSourceProperty.restype = ctypes.c_void_p # already defined
# kTISPropertyUnicodeKeyLayoutData is the key for layout data
kTISPropertyUnicodeKeyLayoutData = cf.CFStringCreateWithCString(None, b"TISPropertyUnicodeKeyLayoutData", 0)
# UCKeyTranslate
# OSStatus UCKeyTranslate(const UCKeyboardLayout *keyboardLayoutPtr,
# UInt16 virtualKeyCode,
# UInt16 kbdModifiers,
# UInt32 kbdType,
# UInt32 flags,
# UInt32 *state,
# UniCharCount maxStringLength,
# UniCharCount *actualStringLength,
# UniChar *unicodeString)
# Define UCKeyboardLayout pointer type
UCKeyboardLayout = c_void_p
carbon.UCKeyTranslate.restype = OSStatus
carbon.UCKeyTranslate.argtypes = [
UCKeyboardLayout, # keyboardLayoutPtr
ctypes.c_uint16, # virtualKeyCode
ctypes.c_uint16, # kbdModifiers
ctypes.c_uint32, # kbdType
ctypes.c_uint32, # flags (kUCKeyTranslateNoDeadKeys)
ctypes.POINTER(ctypes.c_uint32), # state
ctypes.c_uint32, # maxStringLength
ctypes.POINTER(ctypes.c_uint32), # actualStringLength
ctypes.POINTER(ctypes.c_ushort) # unicodeString (UniChar is UTF16)
]
# Constants
kCFStringEncodingUTF8 = 0x08000100
kUCKeyTranslateNoDeadKeys = 0 # Flag to ignore dead keys
kVK_Shift = 0x38 # Virtual key code for Shift
kVK_CapsLock = 0x39 # Virtual key code for CapsLock
# CFRelease
cf.CFRelease.restype = None
cf.CFRelease.argtypes = [CFTypeRef]
def create_cfstring(s):
return cf.CFStringCreateWithCString(None, s.encode('utf-8'), kCFStringEncodingUTF8)
# These are created once, so we don't necessarily need to release them if they live for app duration,
# but good practice would be to manage them. For global constants, it's fine.
kTISPropertyInputSourceID = create_cfstring("TISPropertyInputSourceID")
kTISPropertyInputSourceCategory = create_cfstring("TISPropertyInputSourceCategory")
kTISCategoryKeyboardInputSource = create_cfstring("TISCategoryKeyboardInputSource")
def get_string_from_cf(cf_str):
if not cf_str: return ""
buf = ctypes.create_string_buffer(1024)
if cf.CFStringGetCString(cf_str, buf, 1024, kCFStringEncodingUTF8):
return buf.value.decode('utf-8')
return ""
def get_char_from_current_layout(keycode, is_shift):
# Get the current keyboard layout
source = carbon.TISCopyCurrentKeyboardInputSource()
if not source: return None
layout_data_ref = carbon.TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData)
if not layout_data_ref:
cf.CFRelease(source)
return None
# The layout data is a pointer to UCKeyboardLayout
keyboard_layout_ptr = ctypes.cast(layout_data_ref, UCKeyboardLayout)
# Prepare for UCKeyTranslate
max_string_len = 4 # Max length for a unicode char (e.g., dead keys)
actual_string_len = ctypes.c_uint32(0)
unicode_string = (ctypes.c_ushort * max_string_len)() # UniChar is UTF16
state = ctypes.c_uint32(0)
# Modifiers
# We only care about Shift for now, but UCKeyTranslate can handle others
kbd_modifiers = 0
if is_shift: kbd_modifiers |= (1 << kVK_Shift)
# Get the char
status = carbon.UCKeyTranslate(
keyboard_layout_ptr,
keycode,
kbd_modifiers,
0, # Keyboard type - typically 0 for default
kUCKeyTranslateNoDeadKeys, # Flags
byref(state),
max_string_len,
byref(actual_string_len),
unicode_string
)
# Release the source (it was retained by TISGetSelectedInputSource)
cf.CFRelease(source)
if status == 0 and actual_string_len.value > 0:
# Convert UTF16 to Python string
# UniChar is a C array of unsigned shorts (UTF-16 code units)
char_array = [chr(unicode_string[i]) for i in range(actual_string_len.value)]
return "".join(char_array)
return None
def switch_to_lang(lang_code):
sources_list = carbon.TISCreateInputSourceList(None, False)
if not sources_list:
return False
try:
count = cf.CFArrayGetCount(sources_list)
target_ids = []
if lang_code == 'en':
target_ids = ["com.apple.keylayout.US", "com.apple.keylayout.ABC", "com.apple.keylayout.British"]
elif lang_code == 'ru':
target_ids = ["com.apple.keylayout.Russian", "com.apple.keylayout.RussianWin"]
# First pass
for target in target_ids:
for i in range(count):
source = cf.CFArrayGetValueAtIndex(sources_list, i)
ptr = carbon.TISGetInputSourceProperty(source, kTISPropertyInputSourceID)
source_id = get_string_from_cf(ptr)
if source_id == target:
res = carbon.TISSelectInputSource(source)
return res == 0
# Second pass
search_term = "Russian" if lang_code == 'ru' else "US"
for i in range(count):
source = cf.CFArrayGetValueAtIndex(sources_list, i)
ptr = carbon.TISGetInputSourceProperty(source, kTISPropertyInputSourceID)
source_id = get_string_from_cf(ptr)
cat_ptr = carbon.TISGetInputSourceProperty(source, kTISPropertyInputSourceCategory)
cat = get_string_from_cf(cat_ptr)
if "KeyboardInputSource" in cat and search_term in source_id:
res = carbon.TISSelectInputSource(source)
return res == 0
return False
finally:
if sources_list:
cf.CFRelease(sources_list)