長久以來,輸入法一直是困擾mac用戶的一個問題;不過隨著國內(nèi)廠商的跟進,這種狀況得到了極大的改善。不用自己去折騰什么鼠須管了,狼廠和企鵝都做的不錯。
不過依然有一個問題沒有完全解決:不同程序之間輸入的自動切換。
相信大家一定有切換到item2輸入兩個命令發(fā)現(xiàn)是中文然后按cmd + space切換的尷尬;另外如果你如果使用vi或者emacs,那么就更蛋疼了。造成這種狀況的根本原因在于:輸入法的狀態(tài)是混亂的,我無法明白現(xiàn)在自己處于哪一種輸入環(huán)境,除非我開始打字或者看右上角輸入法的圖標(biāo)。我希望item2,Intellij IDEA,Alfred2永遠(yuǎn)是英文輸入狀態(tài),除非手動切換;其他的程序比如chrome瀏覽器,郵件客戶端保持正常。
打個比方,使用sublime寫代碼,大多數(shù)情況下肯定是英文輸入狀態(tài),寫注釋的時候可能手動切換到中文;但是這里有個問題,這時候如果我切換到其他程序,然后改變了輸入的狀態(tài),再次回到sublime,fuck!怎么又成了中文!
目前解決方案有如下方式:
- mac系統(tǒng)自帶的設(shè)置-> 鍵盤 -> 輸入源 -> 自動使用文稿的輸入源
- 一些輸入法的類似安靜模式的功能
第一種方式,意思就是不同的程序保持獨立的輸入狀態(tài),不會出現(xiàn)你在另外一個程序切換了輸入法的時候再次回來輸入法狀態(tài)就變了。這個開關(guān)很有用,我使用了一段時間,發(fā)現(xiàn)還是怪怪的,有時候并不符合預(yù)期,但是具體場景也搞不明白,反正是一頭霧水,有時候依然會陷入困惑的狀態(tài)。
第二種方式很有意思,應(yīng)該可以滿足很多非程序員的需求。這個安靜模式,打個比方,鼠須管輸入法;這種輸入法其實有幾種輸入模式,如果對于sublime開啟安靜模式,那么在進入sublime程序的時候,會自動切換到英文輸入模式;nice!不過問題就是:如果要切換到中文模式,需要按ctrl或者shift。如果使用一些IDE的話,肯定各種快捷鍵用的飛起,怎么少的了按ctrl和shift,這時候問題就來了,如果我們一不小心在使用某些快捷鍵的時候觸發(fā)了這個輸入法的模式切換功能,那么就蛋疼了:我們需要不停滴按shift切換確保自己處于正確的狀態(tài)。更糟糕的是,如果你發(fā)現(xiàn)自己處于鼠須管的英文輸入模式,想使用中文,然后按了cmd + space 切換,你有可能會切換到系統(tǒng)的英文輸入法,打個字發(fā)現(xiàn)依然是英文!fuck!你不信邪,以為是沒有按到,再猛敲幾次cmd + space,最后你自己處于那個狀態(tài)就暈了。
怎么正確配置輸入法
經(jīng)過這些折騰之后,可以得到輸入法的這么幾條最佳實踐:
- 最基本的原則是要很方便滴知道自己處于哪一種輸入狀態(tài)。如果任何時候清楚這個,那么就是簡單的切換問題了。
- 最好不要使用一個輸入的兩種模式,并使用
shift或者ctrl切換;如上文,某些情況會陷入極度混亂,最好在輸入法之間切換,模式簡單。 - 所有程序輸入法狀態(tài)應(yīng)該有一個恒定的初始態(tài),每次你重新進入這個程序,就會回到初始狀態(tài)。
為什么需要一個恒定的初始狀態(tài)呢?為了明確自己處于哪一種輸入狀態(tài),只需要在每次進入這個程序的時候,不管之前做過什么,它的狀態(tài)是確定的,姑且叫它初始態(tài);然后基于原則2,每次你希望切換的時候cmd + space一下,需要的時候換回來,如果你去了別的程序再回來,狀態(tài)重置為初始態(tài)。
好了分析了這么多,其實要解決的問題就是3一個,我們寫一段小程序。
切換輸入法實現(xiàn)
mac下如果使用objc或者swift切換輸入法很簡單,Apple提供了很詳細(xì)的Text Input Service文檔(現(xiàn)在這個文檔403了,可以使用google的cache訪問);我希望使用python來調(diào)用這些接口,很遺憾的是,pyobjc沒有封裝TIS系列函數(shù),手動使用ctypes模塊來wrap一下:
import ctypes
import ctypes.util
import objc
import CoreFoundation
_objc = ctypes.PyDLL(objc._objc.__file__)
# PyObject *PyObjCObject_New(id objc_object, int flags, int retain)
_objc.PyObjCObject_New.restype = ctypes.py_object
_objc.PyObjCObject_New.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int]
def objc_object(id):
return _objc.PyObjCObject_New(id, 0, 1)
# kTISPropertyLocalizedName
kTISPropertyUnicodeKeyLayoutData_p = ctypes.c_void_p.in_dll(carbon, 'kTISPropertyInputSourceIsEnabled')
kTISPropertyInputSourceLanguages_p = ctypes.c_void_p.in_dll(carbon, 'kTISPropertyInputSourceLanguages')
kTISPropertyInputSourceType_p = ctypes.c_void_p.in_dll(carbon, 'kTISPropertyInputSourceType')
kTISPropertyLocalizedName_p = ctypes.c_void_p.in_dll(carbon, 'kTISPropertyLocalizedName')
# kTISPropertyInputSourceLanguages_p = ctypes.c_void_p.in_dll(carbon, 'kTISPropertyInputSourceLanguages')
kTISPropertyInputSourceCategory = objc_object(ctypes.c_void_p.in_dll(carbon, 'kTISPropertyInputSourceCategory'))
kTISCategoryKeyboardInputSource = objc_object(ctypes.c_void_p.in_dll(carbon, 'kTISCategoryKeyboardInputSource'))
# TISCreateInputSourceList
carbon.TISCreateInputSourceList.restype = ctypes.c_void_p
carbon.TISCreateInputSourceList.argtypes = [ctypes.c_void_p, ctypes.c_bool]
carbon.TISSelectInputSource.restype = ctypes.c_void_p
carbon.TISSelectInputSource.argtypes = [ctypes.c_void_p]
carbon.TISGetInputSourceProperty.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
carbon.TISGetInputSourceProperty.restype = ctypes.c_void_p
carbon.TISCopyInputSourceForLanguage.argtypes = [ctypes.c_void_p]
carbon.TISCopyInputSourceForLanguage.restype = ctypes.c_void_p
def get_avaliable_languages():
single_langs = filter(lambda x: x.count() == 1, \
map(lambda x: objc_object(carbon.TISGetInputSourceProperty(CoreFoundation.CFArrayGetValueAtIndex(objc_object(s), x).__c_void_p__(), kTISPropertyInputSourceLanguages_p)), \
range(CoreFoundation.CFArrayGetCount(objc_object(carbon.TISCreateInputSourceList(None, 0))))))
res = set()
map(lambda y: res.add(y[0]), single_langs)
return res
def select_kb(lang):
cur = carbon.TISCopyInputSourceForLanguage(CoreFoundation.CFSTR(lang).__c_void_p__())
carbon.TISSelectInputSource(cur)
切換輸入法主要是TISSelectInputSource方法,簡單滴調(diào)用這個方法就可以了。使用ctypes包裝這個方法有兩個地方可以借鑒:
pyobjc 轉(zhuǎn)ctypes兼容類型
pyobjc提供的對象是不能直接傳遞給ctypes要包裝的函數(shù)使用的,需要轉(zhuǎn)換成可以識別的類型。每一個pyobjc提供的對象都有一個__c_void_p__()方法,對它調(diào)用這個方法就可以把這個對象轉(zhuǎn)換成一個c_void_p類型
ctypes指針構(gòu)造出pyobjc對象
簡單包裝一下objcruntime里面的new方法,然后可以直接根據(jù)指針new一個對象出來。正如以上代碼的PyObjCObject_New。(新版的pyobjc模塊貌似已經(jīng)包裝了這個方法)
PS:本人第一次包裝objc接口,對于objc以及pyobjc均不熟悉,可能有更優(yōu)雅的方法,請批評指正。
如何自動切換?
要想實現(xiàn)輸入法自動切換,自然是需要在某程序切換到前臺的時候,幫它更改一下輸入法的狀態(tài);如果知道一個程序是不是在前臺呢?最笨的辦法當(dāng)然就是輪詢,但是不夠優(yōu)雅。幸運的是,新的mac系統(tǒng)提供了這個回調(diào)。
class Observer(NSObject):
def handle_(self, noti):
info = noti.userInfo().objectForKey_(NSWorkspaceApplicationKey)
bundleIdentifier = info.bundleIdentifier()
if bundleIdentifier in ignore_list:
print "found: %s active" % bundleIdentifier
select_kb(u'en')
def main():
nc = NSWorkspace.sharedWorkspace().notificationCenter()
observer = Observer.new()
nc.addObserver_selector_name_object_(
observer,
"handle:",
NSWorkspaceDidActivateApplicationNotification,
None
)
AppHelper.runConsoleEventLoop(installInterrupt=True)
這一段代碼可以拿到最前臺運行的application,而且是回調(diào)通知。有兩個地方需要注意:
- Observer對象需要先new出來,(我直接在函數(shù)參數(shù)里面調(diào)用,直接就是segement fault,不知道原因)不能使用python的構(gòu)造對象方式。需要調(diào)用new方法。
- 需要使用AppHelper.runConsoleEventLoop 才能接收到事件,至于為什么見參考。
成果
好了,把上面兩段代碼整合起來;就能實現(xiàn)每次在打開某些程序的時候,自動切換到某個輸入法了!
每次我切換到IDEA敲代碼,輸入法狀態(tài)永遠(yuǎn)都是英文;就算我切換到其他回個郵件,發(fā)個消息切換到了中文,再次回來依然是英文;我手動切換到了中文被打斷了去做了別的事情,再次回來,依然是英文狀態(tài)。我永遠(yuǎn)都知道自己處于什么輸入模式,如果不滿足條件,cmd + space 切換即可。
最后,你可以使用supervisor之類的東西把它加入開機自動運行,這樣,困惑已久的輸入法問題終于得到解決。