みずぴー日記

人間の再起動ボタンはハワイのビーチにある

💎アイマスハッカソン #imas_hack

この記事はMisoca Advent Calendar 2017の21日目として書いた。 なお、ボクはこのあと仕事納めをする。

先日、アイマスハッカソン2017に@と共に参加した。

💓成果物

最終的に以下のものを作った。

🐾やったこと

移動

新幹線の重大インシデントの影響で、ダイヤがふんわりなってるなか東京に向った。 朝の恵比寿は人がいなかった。

f:id:mzp:20171222163122p:plain

スポンサートーク

弥生の人がやよいの画像を出しながらスポンサートークをしていたので聞いた。

アプリデザイン

事前にbanjunと「ライブのときの心拍を再現したいよね」という話をしていたので、それを作ることにした。

banjunのもってきた12インチiPad Proになんとなくのイメージを落書きした。これ以降、iPad Proの出番はなかった。

f:id:mzp:20171222165243p:plain

AppleWatch APIの調査

心拍を再現するためにAppleWatchを振動(haptic)させる方法の調査から始めた。

振動させるにはWKInterfaceDeviceのplay(_:)を呼べばいいのはすぐに分かったが、どれくらいの間隔で振動させれるかは不明だった。 振動時の音を録音して測定しようとしたが、Logicの表示が秒単位でなかったりしてうまくいかなかった。

f:id:mzp:20171216111931j:plain

パラメータを変えて実験したところ、AppleWatchで測定できる心拍数の上限*1以上の速度であることが確認できたので、限界値の探索はあきらめた。

このあたりでお昼を食べた。

watchOSアプリの実装

午後はbanjunがiPhoneアプリの実装をはじめたので、自分はwatchOSアプリの実装を始めた。

機能はスライドに書いたが

などを行なう。

落書き

watchOSアプリの転送は時間がかかるので、ところどころヒマだったので、SketchでiPhoneアプリの画面を考えてた。 来年はこの方面のスキルをつけたい。

f:id:mzp:20171222165316p:plain

iPhoneアプリ

ボクがwatchOSアプリを書く横で、banjunがiPhoneで動く心拍数表示アプリを書いていた。

アプリを作りながら「公演名と日時と心拍数をながめてるだけで、なんともいえない気持ちになりますね」と言ってて、エモかった。ツアー後半にいくにつれどんどん心拍数があがっていくことも明らかになった。

f:id:mzp:20171222170352p:plain

資料作成

終了時間が近づいてきたので、資料作成をはじめた。

事前の練習ができる状況ではなかったので、言いたいことを先にいって、後半はいつ時間切れになってもいい構成にした。 結局、5分ちょうどにおさまった。

f:id:mzp:20171222165701p:plain

LT大会

LT大会兼成果物発表会で作ったものの紹介をした。 はずかしくて見てないが、たぶんYouTube Liveに写ってると思う。

www.youtube.com

発表が前のほうだったので、後半は安心してピザとかビールを楽しんでた。

f:id:mzp:20171216170511j:plain

終了

恵比寿のイルミネーションを見てはしゃぎながら、打ち上げ会場に移動した。

f:id:mzp:20171216210306j:plain

🌟所感

  • ハッカソン中に等身大のアイドルをARで映し出してみたのチームが記念撮影を繰り返してて楽しそうだった。
  • 弥生の人に「アイマスにやよいちゃんってアイドルがでるんでスポンサーしましょうよー」という話をしたが、まさか本当にするとは思っていなかった。
  • 本当にやりたいのはブルーレイとの同期なので、今後もがんばっていきたい。
  • 隣にすわりながら開発すると、conflict解決とかを雑にやれるので楽。

*1:215拍/分以上を測定できたという実績がないのでこれが上限だと予想している

📈入力メソッドの魅力とつらさ

NGK2017Bで発表した。Misoca Advent Calendar 2017 - Qiitaの5日目でもある。

スライド

原稿

イントロ

今日は漢字入力がつらいという話をする。

f:id:mzp:20171204214611j:plain

このなかに入力メソッド、IMEFEPなどと呼ばれるソフトウェアを日常的に使っているひとはいますか? MS-IMEとかATOKとかGoogle日本語入力などが該当する。

おそらく、ほぼ全員が使っていると思う。

f:id:mzp:20171204214615j:plain

入力メソッドの特殊性

日常的に使っているが、入力メソッドはとても特殊なソフトウェアである。

  • すべてのキー入力を受け取る。 パスワード入力欄を含めてすべてのキー入力を取得できる。
  • アプリケーションと協調して変換候補ウインドウなどを表示はするが、メインウインドウのような固有のウインドウは持たない
  • (日本では)ほぼ全員の人が使っている

f:id:mzp:20171204214620j:plain

世界の入力方法

日本語以外の入力には入力メソッドをあまり使わない。例えば、英語の入力に入力メソッドが不要である。

f:id:mzp:20171204214624j:plain

英語以外にもロシア語用にキリル文字が入力できるキーボードもある。キリル文字とは ( ゚д゚) の口のことである。

f:id:mzp:20171204214629j:plain

ハングルの入力

韓国語でも入力メソッドが用いられているが、これも日本語入力とは異なる。

韓国語の表記に使われるハングルは、複数の字母の組み合わせで文字を構成している。 例えば아というハングルは、ㅇとㅏという2つの記号の組み合わせで構成されている。 このそれぞれの記号を文字の元になっているもの、字母と呼ぶ。

それぞれがDキーとKキーに対応しているので、DKと打つことで아が入力できる。

そのため日本語入力の際に使う「変換候補の表示」という動作はない。

f:id:mzp:20171204214633j:plain

漢字入力の特殊性

  • 発音記号を入力する
  • 候補を表示して、そこから入力する文字も選択する

という二段階の変換が必要な文字体系は漢字だけである。正確に言うと、macOSに標準でインストールされている入力メソッドでは、漢字入力だけが変換候補を表示してる。

ので、これは漢字を持つ日本語と中国語の入力で使われている機能である。

f:id:mzp:20171204214637j:plain

入力メソッドを持たない文字体系

一方ではUnicodeには196種類もの大量の文字体系が含まれている。そしてそれには入力方法を持たない文字もいくつかある。

f:id:mzp:20171204214642j:plain

例えば絵文字。

f:id:mzp:20171204214646j:plain

例えば楔形文字

f:id:mzp:20171204214652j:plain

例えばヒエログリフ

f:id:mzp:20171204214658j:plain

入力メソッドの魅力

これらの入力メソッドを作るのはとても魅力的だと思う。使うのは古代エジプトの神官だと思う。

f:id:mzp:20171204214702j:plain

InputMethodKit

macOSの入力メソッド作るためのライブラリはInputMethodKitという名前である。 Appleのライブラリは末尾にkitと付く。

これを使えばヒエログリフ入力メソッドを作れる。作るべきである。

f:id:mzp:20171204214706j:plain

資料不足

InputMethodKitの資料はあまりない。Qiitaで検索してもでてこない。

f:id:mzp:20171204214713j:plain

Appleのドキュメントも「No overview available」と書いてある。無である。 そのためヘッダファイルのコメントや公式サイトから消えたミラーのWebArchiveなどが参考になる。

f:id:mzp:20171204214719j:plain

利用者不足

ドキュメントが少ないのと関連するが使っている人もほとんどいない。 みんな使ってほしい。

f:id:mzp:20171204214723j:plain

動作しないメソッド

一部のメソッドがうまく動作しない。

変換候補を選択するためのメソッドをディスアセンブルしたものを示す。 アセンブリを読むの大変だがよく読むと、スタックにpushしたあとpopしているだけである。無である。

「なぜ???」と思いながらバグレポをしたら「既知のバグ(duplicated)」と返ってきた。直してほしい。

f:id:mzp:20171204214728j:plain

まとめ

まとめると以下のようになる。

  • 漢字入力は実は特殊
  • でもいろんな使い道がありそうだしInputMethodKitみんな使ったほうがよい
  • つらい

f:id:mzp:20171204214733j:plain

その他

https://github.com/mzp/EmojiIM でいろいろやっている。見てね。

f:id:mzp:20171204214737j:plain

🍣寿司、パーサー、Scala.js

@に誘われたので、scala-js-ts-importerハッカソンに参加した。

🐾経緯

NGK2017Bに行ったら、パーサを書くとピザか寿司がもらえるハッカソンが告知されていた。 寿司もパーサーも好きなので、@と参加することにした。

f:id:mzp:20171204223753p:plain

✨成果

準備する時間はなかったので、scala-js-ts-importerをダウンロードしただけで向かった。当日はScala.jsとかTypeScriptの話を他の人に聞きながらプルリクエストをいくつか出した。

その後、多少の修正が必要だったもののすべてマージされた。 🎉🎉🎉

パーサーいじるのたのしかったし、いろいろと直すべき箇所があってscala-js-ts-importerはいい題材だった。

🍣寿司

昼の出前寿司はうまかった。

f:id:mzp:20171203115552j:plain

その他、所感

  • 来栖川のオフィス、広くてキレイでよかった。いいマッサージチェアも置いてあってよかった。
  • Wifiのアクセスポイントがマルチとセリオとイルファでよかった。@が「マルチとセリオは知っているけど、イルファは...???」と言っていて、老っぽかった。
  • 打ち上げで「湯治をしながらリモートワークするというのはどうか。アーロンチェア買うより肩と腰にいいのではないか」という話をしていた。 そのうちやりたい。

⌨️キーボード配列の取得

入力メソッドはキー入力イベントをテキスト入力に変換する。

キーボードのキーが押されたときに、どのキー入力イベントが発生するかはmacOSの設定によって変更できる。 通常はキーに印字されている文字と一致するようにQwerty配列が用いられるが、Dvorak配列などのそれ以外の配列にも変更できる。

そのためmacOS標準の日本語入力では、どのキーボード配列を使うかを設定できる。

f:id:mzp:20171124114611p:plain

🐙ソースコード

💎TISInputSource

利用可能なキーボードを取得する関数は、Carbon frameworkのHIToolboxで提供される。HIToolboxではキーボード配列や入力メソッドなどを総称して入力ソース(InputSource)と呼ぶ。

TISCreateInputSourceList は条件に合致したものを入力ソース取得する。 入力メソッドで使えるキーボードを取得するには種別がキーボード配列であり、ASCII文字を入力できる入力ソースを取得すればよい。

let conditions = CFDictionaryCreateMutable(nil, 2, nil, nil)
CFDictionaryAddValue(conditions,
                     unsafeBitCast(kTISPropertyInputSourceType, to: UnsafeRawPointer.self),
                     unsafeBitCast(kTISTypeKeyboardLayout, to: UnsafeRawPointer.self))
CFDictionaryAddValue(conditions,
                     unsafeBitCast(kTISPropertyInputSourceIsASCIICapable, to: UnsafeRawPointer.self),
                     unsafeBitCast(kCFBooleanTrue, to: UnsafeRawPointer.self))

guard let array = TISCreateInputSourceList(conditions, true) else {
       return nil
}
return array.takeRetainedValue() as? [TISInputSource]

取得したキーボード配列の属性はTISGetInputSourcePropertyで取得する。

// 入力ソースのIDを取得する
TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID)

// キーボード配列の名前を取得する
TISGetInputSourceProperty(inputSource, kTISPropertyLocalizedName)

🙊キーボード配列の種類

ASCII文字が入力可能なキーボード配列は2種類存在する。

1種類目はDvorak、ColemarkのようなQwertyと同様のキーを持ちながら配置のみが違うものである。macOSはこれらは英語入力用の配列として扱う。

f:id:mzp:20171124115052p:plain

もう1種類は、フランス語キーボードのようにùなどの特別な文字を入力できるキーボードである。これらは英語以外のキーボードとして扱う。

f:id:mzp:20171124115042p:plain

この2種類のキーボード配列を区別する方法は公開されていない。しかし、TSMInputSourcePropertyScriptCodeプロパティを取得すれば可能である。このプロパティは日本語入力の設定画面のコードから発見した。

let r = TISGetInputSourceProperty(inputSource, "TSMInputSourcePropertyScriptCode" as CFString)
let n = unsafeBitCast(r, to: NSInteger.self)

// 39なら英語
return n == 39

Mojaveで若干挙動が変更になった。詳細は🏜InputMethodKit for Mojave - みずぴー日記を参照すること。

💄設定画面

HIToolboxのままだと、CoreFoundationの型が必要になるなど、扱いが面倒である。 そこで TISInputSource を拡張し、Swiftからも利用しやすくする。

extension TISInputSource {
    var localizedName: String {
        return unsafeBitCast(
            TISGetInputSourceProperty(self, kTISPropertyLocalizedName),
            to: NSString.self) as String
    }

    var inputSourceID: String {
        return unsafeBitCast(
            TISGetInputSourceProperty(self, kTISPropertyInputSourceID),
            to: NSString.self) as String
    }

    var scriptCode: Int? {
        let r = TISGetInputSourceProperty(self, "TSMInputSourcePropertyScriptCode" as CFString)
        let n = unsafeBitCast(r, to: NSInteger.self)
        return n
    }

    class func keyboardLayouts() -> [TISInputSource]? {
        let conditions = CFDictionaryCreateMutable(nil, 2, nil, nil)
        CFDictionaryAddValue(conditions,
                             unsafeBitCast(kTISPropertyInputSourceType, to: UnsafeRawPointer.self),
                             unsafeBitCast(kTISTypeKeyboardLayout, to: UnsafeRawPointer.self))
        CFDictionaryAddValue(conditions,
                             unsafeBitCast(kTISPropertyInputSourceIsASCIICapable, to: UnsafeRawPointer.self),
                             unsafeBitCast(kCFBooleanTrue, to: UnsafeRawPointer.self))

        guard let array = TISCreateInputSourceList(conditions, true) else {
            return nil
        }
        guard let keyboards = array.takeRetainedValue() as? [TISInputSource] else {
            return nil
        }
        return keyboards.sorted {
            $0.localizedName < $1.localizedName
        }
    }
}

入力メソッドの設定画面で作成した設定画面にキーボード配列選択のためのポップアップを追加する。

public class Preferences: NSPreferencePane {
    private let store: SettingStore = SettingStore()
    private lazy var keyboardLayouts: [TISInputSource]? = TISInputSource.keyboardLayouts()?.filter { $0.scriptCode == 39 }

    override public func mainViewDidLoad() {
        let keyboard = NSPopUpButton() ※ {
            for layout in keyboardLayouts ?? [] {
                $0.addItem(withTitle: layout.localizedName)
            }
        }
        …
    }
}

選択したキーボード配列をNSUserDefaultに保存するように変更する。さらに起動時に保存した内容を読み込むようにする。

keyboard.reactive.selectedIndexes.observeValues {
        if let layout = self.keyboardLayouts?[$0] {
                self.store.setKeyboardLayout(inputSourceID: layout.inputSourceID)
        }
}
if let index = keyboardLayouts?.index(where: { $0.inputSourceID == store.keyboardLayout() }) {
       keyboard.selectItem(at: index)
}

📝 入力メソッド

入力メソッドでキーボード配列を指定するには IMKTextInputoverrideKeyboard(withKeyboardNamed:) を用いる。

クライアントアプリケーションが切り替わったときに呼ばれる activateServer と入力モードが切り替わったときに呼ばれる setValue(_:, forTag:,client) でキーボード配列を指定する。

extension EmojiInputController /* IMKStateSetting*/ {
    override func activateServer(_ sender: Any) {
        NSLog("%@", "\(#function)((\(sender))")

        guard let client = sender as? IMKTextInput else {
            return
        }
        client.overrideKeyboard(withKeyboardNamed: SettingStore().keyboardLayout())
    }

    override func setValue(_ value: Any, forTag tag: Int, client sender: Any) {
        NSLog("%@", "\(#function)(\(value), forTag: \(tag))")
        guard let value = value as? NSString else {
            return
        }
        guard let sender = sender as? IMKTextInput else {
            return
        }
        sender.overrideKeyboard(withKeyboardNamed: SettingStore().keyboardLayout())
    }
}

🖼入力メソッドの設定画面

入力メソッドはシステムに組込まれるので、ユーザとやりとりするための画面を持たない。 しかし、入力メソッドの挙動を設定するための設定画面は必要である。

macOS標準の日本語入力の設定画面はキーボード設定内に組み込んでいる。

f:id:mzp:20171118155509p:plain

この画面を実現するための非公開APIについて説明する。

f:id:mzp:20171118160629p:plain

🐙ソースコード/English version

🙊入力メソッドの設定画面

入力メソッドを選択した際、システム環境設定が入力メソッドに含まれる Resources/Preferences.prefPane を設定画面としてロードする。

Preferences.prefPane は、設定画面を提供するためのプラグイン形式であるPreference paneである。 Preference paneの詳細はPreference Pane Programming Guideに詳しい。

設定画面はシステム環境設定の一部としてロードされるので、直接入力メソッドとは別プロセスで動作する。 ユーザーが変更した内容は UserDefaults などを経由して入力メソッドと共有する。

f:id:mzp:20171118160140p:plain

(Preference Pane Programming Guide: Figure 1 Plug-in architecture of preference panesより引用)

設定画面の追加

XcodeのPreference Paneテンプレートを用いて設定画面用のターゲットを追加する。設定画面のファイル名はPreferences.prefPaneに固定されているので、ターゲット名はPreferences にする。

f:id:mzp:20171118160450p:plain  入力メソッドをビルドする際に設定画面もビルドされるように、入力メソッドのTarget Dependenciesに設定画面を追加する。

f:id:mzp:20171118160549p:plain  ビルド時に設定画面が入力メソッド内に埋め込まれるようCopy Fise Phaseを追加する。コピー先はResourcesディレクトリとする。

f:id:mzp:20171118160616p:plain

これでキーボード設定にて入力メソッドを選択した際に、設定画面がロードされるようになる。

🕊Swiftの利用

Preference Paneテンプレートで生成されたファイルはObjective-Cで記述されているが、Swiftで書き直したい。Swiftで書き直すために、実行時にlibswiftCore.dylibなどのSwiftの標準ライブラリをロードできるようにする必要がある。

macOSの実行ファイルは、rpathと呼ばれるディレクトリから動的リンクライブラリを探索する。 通常はこのrpathに@executable_path/../Frameworks が含まれている。 @executable_path は実行ファイルのパスに解決されるため、アプリケーション内のFrameworksディレクトリに依存する動的リンクライブラリを同梱する。

f:id:mzp:20171118161647p:plain

しかし設定ファイルはシステム環境設定に読み込まれるため、@executable_path/Applications/System Preferences.app/Contents/MacOS/System Preferences に解決される。そのため、Swiftの標準ライブラリが意図した通りにロードされない。

ロードされるファイルのパスは @loader_path で表現できる。設定画面のBuild Settingsを変更し@loader_path/../../../../Frameworks をRunpath Search Pathsに加える。  f:id:mzp:20171118162221p:plain

../../../../ を用いるのは以下のように入力メソッドとFrameworksを共有するためである。  f:id:mzp:20171118162239p:plain

🍫CocoaPodsの利用

CocoaPodsはPreference paneに対するフレームワークの埋め込みに対応していない。そのため、podを利用したい場合は、以下のようにホストとなるアプリケーションに埋め込む必要がある。

platform :osx, '10.11'
       
abstract_target 'App' do
  pod '※ikemen' <- 使える

  target 'EmojiIM'
  target 'Preferences' do
    pod 'NorthLayout'  # <- これは使えない
  end
end

🔑設定の共有

ユーザーが変更した設定は UserDefaults に保存するのが一般的である。 しかし入力メソッドはサンドボックス内で実行されるため、他のアプリケーションとUserDefaultsを共有できない。サンドボックスの詳細についてはApp Sandbox Design Guideが詳しい。

f:id:mzp:20171118163015p:plain

(App Sandbox Design Guideより引用)

設定画面がロードされるシステム環境設定はサンドボックス外で実行されているため、設定を共有するためにはサンドボックスを越える必要がある。 サンドボックスのEntitlementsで例外設定をすることで、サンドボックス外にアクセスできるようにする。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>com.apple.security.app-sandbox</key>
        <true/>
        <key>com.apple.security.temporary-exception.shared-preference.read-only</key>
        <string>jp.mzp.inputmethod.EmojiIM</string>
</dict>
</plist>

これを用いて以下のようなサンドボックスの内外で設定を共有できる UserDefaults のラッパーを作成する。

class SettingStore {
    // Sandbox内のアプリケーションのbundle identifier
    private let kSuiteName: String = "jp.mzp.inputmethod.EmojiIM"

    // システム環境設定にロードされているかどうかを判定する
    private var inSandbox: Bool {
        return Bundle.main.bundleIdentifier == kSuiteName
    }

    private lazy var userDefaults: UserDefaults? = {
        if inSandbox {
            // サンドボックス内ならUserDefaults.standardを使う
            return UserDefaults.standard
        } else {
            // サンドボックス外なら、名前を明示的に指定する
            return UserDefaults(suiteName: suiteName)
        }
    }()

    // userDefaultsを用いて設定を書き込む
    func setKeyboardLayout(inputSourceID: String) {
        userDefaults?.set(inputSourceID, forKey: "keyboardLayout")
        userDefaults?.synchronize()
    }

    // userDefaultsを用いて設定を読み込む
    func keyboardLayout() -> String {
        return (userDefaults?.value(forKey: "keyboardLayout") as? String) ?? "com.apple.keylayout.US"
    }
}

⌚️UTC生活

iPhone/AppleWatchのタイムゾーンUTCに切り替えて一ヶ月が経過した。

f:id:mzp:20171112200832p:plain

✨きっかけ

1ヶ月前に格闘していたpluginkitの時刻表示がUTCだった。

過去に、他のソフトウェアでもログがUTCで出力されたことなどを思い出したので、せっかくなので普段使うタイムゾーンUTCに切り替えた。

🔀 切り替えたもの

👍成果

  • 0:00、12:00などのキリがいい時間はすぐにJSTに変換できるようになった
  • キリがいい時間±2時間くらいもすぐ変換できるようになった

要はあまり成果がない。

🤔知見

  • 時報botとして@が使えると思いきや、イギリスはサマータイムを導入しているので使えない。
  • Twitter/Githubなどは相対時刻表示(3日前など)をしてるので、使い勝手は変わらない。
  • Yahoo!乗換案内はローカルタイムで検索するので、使えなくなる。Web版の乗換案内は大丈夫。
  • 寝起きに時計を見ても、まだ寝れるのか、起きたほうがいいのか判断がつかない。自分の感覚を信じる。
  • JSTで生活している人と待合せ時刻を決めるときは注意が必要。
  • 「今何時?」と聞かれたときに「UTCだと11時」とか答えてはいけない。
  • 困ったらSiriに「東京は何時?」と聞くとよい。

🙊入力モードの非公開機能

🌓入力モードは、Technical Noteやヘッダファイルに記載されていた内容である。 実際は、その内容以外にも文書化されていない機能がいくつか存在している。

📝文末文字の指定

macOSにはスペースを二回押すことでピリオドを入力する機能がある。(参考: macOS 10.12 SierraではiOSと同じスペースバーを2回押しでピリオド入力などの入力支援をサポート。 | AAPL Ch.)

f:id:mzp:20171029000928p:plain  ピリオドとして使用される文字を TISDoubleSpaceSubstitution でカスタマイズできる。入力モードごとに定義する。

f:id:mzp:20171029001004p:plain

<key>ComponentInputModeDict</key>
<dict>
  <key>tsInputModeListKey</key>
  <dict>
    <key>jp.mzp.inputmethod.EmojiIM</key>
    <dict>
      <key>TISDoubleSpaceSubstitution</key>
      <string>🍣</string>

👩‍👩‍👧‍👦 入力モードの統合

通常、入力モードは個別に追加・削除する。しかし、日本語入力ではひらがな入力とカタカナ入力、英数入力は基本的に併用するので、個別に追加・削除できる必要はない。

その場合、TISUnifiedUIForInputMethodEnabling を定義することで一括で追加削除できるようになる。

<key>ComponentInputModeDict</key>
<dict>
  <key>TISUnifiedUIForInputMethodEnabling</key>
  <true/>
  <key>tsInputModeListKey</key>
  <dict>
    <key>jp.mzp.inputmethod.EmojiIM</key>
    <dict>
    ....

この機能は、現在、日本語入力でのみ利用されている。

Mojaveで若干挙動が変更になった。詳細は🏜InputMethodKit for Mojave - みずぴー日記を参照すること。

❓未解決

以下のキーの用途は、現時点で分かっていない。

  • TISCapsLockIsSwitch
  • TISCapsLockResetOnModeSwitch
  • TISIconIsTemplate
  • TISParticipatesInTouchBar

🐙ソースコード

Secret of input modes by mzp · Pull Request #16 · mzp/EmojiIM