はじめに
エディタでは「英数」、Slackやブラウザでは「日本語」——アプリを切り替えるたびに かな/英数 キーを押し直すの、地味にストレスじゃないですか?
私は1日に何百回もこの切り替えをやっていることに気づき、「アプリごとに入力ソースを自動で固定できないか」と思って個人開発で仕組みを作りました。この記事ではその実装の考え方を共有します。
やりたいこと
- VS Code・ターミナルにフォーカス → 自動で 英数
- Slack・メモ・チャット → 自動で 日本語
- ブラウザは開いているサイトごとに切り替えたい(GitHubは英数、ニュースサイトは日本語…)
実装の方針
1. アプリの切り替えを検知する
フォアグラウンドアプリが変わったことは NSWorkspace の通知で取れます。
NSWorkspace.shared.notificationCenter.addObserver(
self,
selector: #selector(activeAppChanged(_:)),
name: NSWorkspace.didActivateApplicationNotification,
object: nil
)
@objc func activeAppChanged(_ note: Notification) {
guard let app = note.userInfo?[NSWorkspace.applicationUserInfoKey]
as? NSRunningApplication,
let bundleID = app.bundleIdentifier else { return }
// bundleID ごとに、設定された入力ソースへ切り替える
switchInputSource(for: bundleID)
}
2. 入力ソースを切り替える
入力ソースの取得・選択は Carbon の TextInputSources API(TISInputSource)を使います。
import Carbon
func selectInputSource(id: String) {
guard let list = TISCreateInputSourceList(nil, false)?
.takeRetainedValue() as? [TISInputSource] else { return }
for source in list {
guard let ptr = TISGetInputSourceProperty(source, kTISPropertyInputSourceID)
else { continue }
let sourceID = Unmanaged<CFString>.fromOpaque(ptr).takeUnretainedValue() as String
if sourceID == id {
TISSelectInputSource(source)
return
}
}
}
bundleID → 入力ソースID のマッピングを持っておけば、アプリを切り替えるだけで勝手に入力ソースが変わります。
3. ブラウザはURL単位で切り替える
ブラウザは「アプリ」では区別できないので、アクティブタブのURLが必要です。これは Accessibility(AX)API でフロントのウィンドウからアドレスバーの値を読み取る方針になります(要・アクセシビリティ権限)。ホスト名でルールを引いて切り替えます。
ハマりどころ
- 切り替え直後に通知が連続で飛ぶことがあるので、短時間のデバウンスを入れないと入力ソースがチラつきます。
- ユーザーが手動で切り替えたときは尊重したい、など「自動」と「手動」の競合をどう扱うかが地味に難しいポイントでした。
ついでに:第三者製マウスのスクロールも
入力まわりを触っているついでに、サードパーティ製マウスの「カクカクするスクロール」を、CGEventTap で慣性付きのスムーズスクロールに変換する処理も入れました。サイドボタンのリマップもアプリごとに設定できます。
個人開発のアプリとして公開しています
この仕組みを LinguaX というmacOSアプリにまとめて公開しています。入力ソースの自動切り替え + 第三者製マウスの強化を、メニューバー常駐の軽量アプリ(無料プランあり)として動かしています。
実装の詳細や「こういうケースはどうしてる?」など、コメントで気軽に聞いてください。
まとめ
- アプリ切り替えの検知は
NSWorkspace.didActivateApplicationNotification - 入力ソースの操作は
TISInputSource(Carbon) - ブラウザのURL判定は Accessibility API
- 自動切り替えはデバウンスと手動操作との競合処理が肝
みなさんは入力ソースの切り替え、どうやって運用していますか?よければコメントで教えてください 🙌