iTerm2 + AquaSKK
要約
iTerm2のmasterにAquaSKK対応入った。l とかで英数に切り替えようとしたときなどに、文字が入力されてしまう問題への対処。デフォルトはOFFになってるので、Preferences -> AdvancedでAquaSKKと検索して、Yesに設定する必要がある。
— 武藤スナイパーカスタム (@__tai2__) May 15, 2016
経緯
iTerm2 + AquaSKKには、l/L/q/Qで入力モードを切り替えるとその文字も入力されてしまう問題がある。(参考: iTerm2/Apple TerminalでAquaSKKを使う - みずぴー日記)
これを修正するPull requestを @__tai2__が作ってくれたので、取り込むための各種議論に参加していた。
そのPull requestが本日(2016/5/15)masterに取り込まれたので、次回のNightly BuildsやTest Releasesとともにリリースされるはず。
闇
「このAppleのドキュメントには、キーが押されたらテキストが入力されるか、ショッートカットキーが発動するって書いてある」「こっちのドキュメントにはInputMethodにそんな制限は書いてないけら、どちらも呼ばないのもありのはずだ」という闇のような議論がはじまった
— mzp (@mzp) 2016年4月23日
OS XのTextEditでnを長押ししたときの挙動がInputMethodによって違うってマジかよ...。日本語入力の英数モードだとnがリピートされて、US配列モードだとñ や ń とかの選択肢がでる。 pic.twitter.com/KQhJZvBjXH
— mzp (@mzp) 2016年5月13日
🇯🇵 SJIS/EUC-JPのテキストファイルをQuick Lookする
QuickLook便利なんだけど、SJISやEUC-JPのテキストファイルが見れないのが面倒だな
— mzp (@mzp) 2016年5月7日
SJIS/EUC-JPのテキストファイルをQuick Lookできないのが不便なので、プラグインを書いた。
ダウンロード
https://github.com/mzp/qltext-jp
実装
Quick Lookされたときにエンコードの自動判定を行ない、その後の表示はシステムにまかせている。
実装は以下のプラグインを参考にした。
また文字コードの判定は以下のコードを用いている。
他の方法
xattr
xattr
で拡張属性を設定すれば特にプラグインをいれなくても、Quick Lookできる。(参考: Mac の Quick Look をちょっとだけ快適に – xattr 編 – (フェンリル | デベロッパーズブログ))
xattr -w com.apple.TextEncoding "SHIFT_JIS;2561" README.txt
が、毎回これをやるのは大変なので、Quick Lookプラグインを作成した。
quicklook-jptxt
GitHub - ento/quicklook-jptxt: Quick Look plugin for public.plain-text with better encoding handling. を使えば同等のことができる。文字コード判定が微妙に違うくらい。
これでダメな理由は特にないが、まあ作りかけてしまったので完成させてしまった。
その他
最近はSJISのファイルってあまりないよね。
sjisのファイルないけど
— ばんじゅん(!!) (@banjun) 2016年5月7日
AquaSKK 4.4.0: 互換性のための設定画面追加
AquaSKKが正しく動作しないアプリケーションのための設定画面を追加し、AquaSKK 4.4.0をリリースした。
ダウンロード
https://github.com/codefirst/aquaskk/releases/tag/4.4.0
変更点
一部のアプリケーションでは、l/Lによる入力モード切り替えが動作しないため、特別な回避策が必要となる。
これまでのバージョンでは、回避策を適用するアプリケーションリストをコード中に直接記述していた。 そのため、アプリケーションリストを更新するためにAquaSKK本体の更新が必要となっていた。
この対応がだんだんと厳しくなってきたので、設定画面からアプリケーションリストを更新できるようにした。
余談: 名称の由来
基本的に回避策は意味がよくわからないことをしているため、設定画面のラベル名を決めるのが大変だった。 最終的に結城(@hyuki)氏の案を採用した。
@mzp 露払後確定
— 結城浩 (@hyuki) April 23, 2016
余談: お願い
この設定はあくまで回避策なので、できる限りそのアプリケーション側にバグレポートを送信してほしい。 闇に闇を積み重ねるのはよくない。
SKK辞書の闇への対応状況
SKK-JISYO.lispで書いたようにSKKの辞書形式にはいくつかの闇(=歴史的経緯による複雑な仕様)を抱えている。
この闇に対して、SKKの各実装がどう対応をしているかを調べた。
調べた実装
- fcitx-skk 0.1.1-1.1 *1
- uim-skk 1.8.6-15
- eskk.vim e990b81
- CorvusSKK 2.3.3
- SKK日本語入力FEP β0+9i版
- SKK日本語入力FEP β0+9i版 + SKKGate β0+2i版
- AquaSKK 4.3.5
- FlickSKK 1.4.2
concatへの対応
SKK辞書の書式上、/
や;
を含む変換候補は登録できない。 そのため、L辞書には以下のようにconcatを用いて登録されている。
dosv /(concat "DOS\057V")/
対応済
concatによるエスケープを理解し、意図した変換結果を出力する。
未対応
変換結果に(concat ".....")
がそのままでてしまう。
/や;を含んだ単語の登録
ユーザが/
や;
を含んだ単語を登録した場合もconcatによるエスケープが必要である。
対応済
対応済?
以下のように独自の形式でエスケープを行なう。 単語登録は行なえるが、他のSKK実装とは互換性がない。
dosv /DOS[2f]V"/
- AquaSKK
- FlickSKK
未対応
/
や ;
をそのまま登録してしまう。単語を登録しても変換できない。
concat以外のEmacsLisp式
SKKの辞書は任意のEmacsLisp式を含めることができる。そのためlisp辞書には以下のようなエントリが登録されている。
time /(current-time-string)/
部分対応
pwdなどの一部のエントリには対応していないが、Lisp辞書の大半に対応している。
部分対応 その2
current-time-string、pwd、skk-versionの3つの関数のみに対応している。 CorvusSKK等に比べるとかなり限定的。
- fcitx-skk
未対応
数値エントリ
SKKの辞書は # を含む見出し語を特別扱いする。例えば、以下のエントリは「3かい」や「10かい」とマッチする。
#かい /#1回/#0回/#3回/#2回/
変換時、 #<n>
は以下のように変換される。
#0
: 半角数字。(例: 1024)#1
: 全角数字。(例: 1024)#2
: 漢数字で位取りあり。(例: 一〇二四)#3
: 漢数字で位取りなし。(例: 千二十四)#4
: 再変換。見出し語中の数字そのものをキーとして辞書を再検索する。*2#5
: 大字。(例: 壱阡弐拾四)#8
: 桁区切り。(例: 1,234) *3#9
: 将棋の棋譜入力用。(例: 8五)
詳細はSKK Manual: 数値変換に記載されている。
対応済
部分対応
未対応
数値エントリの変換に対応していない。
追記
@mzp corvusskkですが(current-time-string)と数値変換#9については一応使えるはずなので確認願えますでしょうか。作ってみて感じたのですが、下手に足を突っ込むと正に闇なのでconcatへの対応のみにしておくと精神安定上良いかなと思いました。
— Ν (@corvussolis) 2016年5月2日
CorvusSKKの対応状況に誤認があったので、修正した。
追記 その2
@mzp ご紹介ありがとうございます。SKK日本語入力FEPでは数値変換やLispに対応していないと書かれていますが、JavaScriptによる拡張モジュールを入れると変換できるようになります。簡易Lispエミュレータはlisp分離前のL辞書に入っていた候補に対応してます
— ガイアにcoがもっと輝けと囁いている (@coexe) 2016年5月2日
SKK日本語入力FEPの拡張機能のことを認識していなかったので、追記した。
所感
SKK-JISYO.lisp
NL名古屋 - connpassでSKK辞書の話をした。 要約すると、特定の文字をエスケープするだけでEmacsLispが必要になってつらいという話だった。
Transiruで発表したので、発表時の音声等はこちらで聞ける。終盤早口になってしまったので恥かしい。声優吹き替えオプションが欲しい。
原稿
前提
自己紹介
こんにちは。 mzpです。 タイトルにあるように日本語入力システムとLispについて話したいと思います。日本語のNと、LispのLでNLです。
何の話?
一言で日本語入力システムといってもたくさんの種類がありますし、Lispにもたくさんの種類がありますね。
ので、具体的に言うと、今日はSKKとEmacsLispの話をします。LispといってEmacsLispを持ちだすのは微妙な気がしますが、まあLispと名乗ってるわけですしOKでしょう。
SKKとは
このSKKについてですが、これはだいぶ独特な日本語入力システムです。通常の日本語入力システムがやるような作業を利用者におしつけることで、高速かつ高精度の日本語変換を実現しています。
入力例
分かりづらいと思うので、入力例を見てみます。例として「今日は雨です」と入力してみる場合です。
普通の日本語入力システムでは「きょうはあめです」と入力したのち、スペースを押して、変換候補を表示します。お馴染みですね。
SKKでは、漢字変換を始める前にShiftを押します。
Kyou
そして、複数の単語をまとめて変換することはできないので、ここでスペースをおして候補を表示します。
ひらがなはそのまま入力すればOKです。特に変換は必要ありません。
ha
また漢字なのでShiftを押します。
Ame
そして、スペースを押して候補を表示し、確定させます。
そして、またひらがなはそのまま入力します。
desu
SKKの特徴
このように
- 単語ごとでしか変換できない
- 漢字に変換するかどうかを自動で判断しない
といった特徴があります。
一見使いづらいだけのように見えますが、変換する単語を完全に自分で制御できるので、慣れれば離れられなくなる中毒性があります。
SKKの実装
中毒性が高いソフトウェアのため、現在では様々なプラットフォームで利用できます。
- Windows: SKKFEP、CorvusSKK
- Mac: AquaSKK
- iOS: FlickSKK
- Linux: iBus-SKK, UIM-SKK, SCIM-SKK
- Vim: skk.vim
- Emacs: ddskk
このAquaSKKとFlickSKKは僕がメンテしてます。
SKKの歴史
元となったSKK(通称: 本家SKK)は、1987年に、東北大学教授(当時)佐藤雅彦によって開発されました。
この本家SKKは、Emacs上のプログラムとして開発されました。 つまりEmacsLispを用いて開発されていました。
いやー、Emacsはすばらしいソフトウェアですね。 実にすばらしい。
辞書ファイル
とEmacsを十分に褒めたところでつらい話をはじめます。
ひらがなを漢字に変換するためには、ひらがなと漢字の対応表が必要になります。この対応表は辞書と呼んでいます。
そしてこの辞書ファイルが闇を貯めこんでいます。
辞書ファイルの書式
辞書ファイルはテキストファイルになっており、各行が以下のような書式になっています。
なごや /名古屋;愛知/那古屋/
左から順に「見出し語」「変換候補」「アノテーション(要するに補足情報)」となっています。変換候補間は/で区切られていて、アノテーションとは;で区切られています。
自然な感じがしますね。
エスケープ
しかし、この書式をじっと見てると、いくつか使えない文字があることが気づきます。そう、変換候補として「;」や「/」を含むことができないのです。
例えば「owata」の変換候補として「(^o^;)/」 とかを登録したい場合はどうしたらいいでしょう。
対応方法1: エスケープ
いくつかの対応方法が考えられます。 例えば、文字列リテラルのように\でエスケープすればよさそうですね。
owata /\\(^o^\;)\//
対応方法2: 別の文字への置換
あるいは別の文字に置換してしまうのもいいでしょう。
例えば、"[ASCIIコード]"のような記法を採用すると、;のASCIIコードは0x3b、/は0x2fなので以下のようになります。
owata /\(^o^[3b])[2f]/
対応方法3: 全角文字
あきらめてよく似た文字を使うことで誤魔化してもいいです(?)。
owata /\(^o^;)//
いやよくないでしょ...。
正解
みなさんだったらどうしますか?ちょっとだけ考えてみてください。
....
考えましたね。 ではSKKがどうしたかを見てみましょう。
owata /(concat "\(^o^\059)\057")/
S式の文字列リテラルを使ってエスケープする、が正解でした〜。
まじかよ...って感じですね。
その他の便利機能
日本語入力システムを使ってると、いろいろ便利な変換したくなってきます。
例えば、
- todayを今日の日付に変換したい
- nowを今の時刻に変換したい
- 5feetをメートルに変換したい
- 元号を変換したい
などです。
これらはすべてEmacsLispを使って実現できます! すごい!
まじかよ...って感じですね。
さらにすごいことに以下のような変換もEmacsLispで実現されています。
- 画面幅いっぱいの線
まじかよ...って感じですね。
実装
Lispの実装 これらの機能は使う分にはまだいいんですが、実装者としては悪夢です。
つまりSKKを実装するには、EmacsLisp処理系を実装しなければいけません。 より具体的に言うと、AquaSKKとFlickSKKはボクがメンテしていく上で、ボクがEmacsLisp処理系を実装しなければいけません。
まじかよ...って感じですね。
SKK-JISYO.lisp
なげいていてもしょうがないので、現実を確認してみましょう。
実は「今日の日付」のような複雑なEmacsLispの式を含む変換候補は、通常の辞書とは別で管理されています。余談ですが、この別に管理されている辞書はLisp辞書と呼ばれていますが、このLisp辞書って単語もだいぶ愉快ですね。
そのため通常の辞書だけを利用すると割り切ってしまえば、フルセットのEmacsを実装者するのは避けれます。通常の辞書に残っているEmacsLispの式はconcatによる文字のエスケープだけなので、これに対応するだけで十分です。
実装状況
SKKの各実装がこれにどのように対応しているか見てみましょう。
AquaSKK/FlickSKK
AquaSKK/FlickSKKは割り切った対応をしています。外部の辞書ファイルに含まれるconcatについては何もしません。つまり変換するとこんな変換結果がでてきます。(例: dosv)
そして、ユーザが入力した内容した結果については、別の文字列に置き換えることでエスケープしています。先ほど述べた別の文字への置換のような形式です。
owata /\(^o^[3b])[2f]/
後者はともかくconcatがでてくるのは微妙なので直したいとは思っています。
ibus-skk/scim-skk
ibus-skk/scim-skkは、concatのみの実装をしていてソツがない感じになっています。
CorvusSKK
一方、CorvusSKKはここの実装をがんばっていて、いくつかの関数もサポートしています。すごいです。
その他
Lispの話はこれくらいですが、実は辞書ファイルにはまだいくつかの闇が存在しています。
辞書の並び順
これまでは辞書の各行の話をしてきましたが、これらの行は見出し語をキーとしてソートされています。いわゆる辞書順です。辞書ですからね。
このときの比較関数としてはEmacsLispの string< が使われます。....またEmacsLispか。
そしてこのstring<は文字コード順で文字列を比較します。 はい、ぞわぞわしてきましたね。
なんの文字コードだよ、って感じですね。SKKでは伝統的にはEUC-JPが使われますが、最近はUTF-8を使うことも多いです。というかボクは使っています。
UTF-8の比較というとアレがありますね。 濁点問題です。NFCとNFDという2種類の形式があります。濁点を合成された文字としてもつか、分解された文字としてもつかの違いです。
世の中的にはNFCを使うことが多いんですが、ボクのメンテしているApple系のプラットフォームだとNFDが主に使われてたりします。
どちらが正しいとか優れているとかはありませんが、並び順には影響するので注意深く扱う必要があります。ボクはドハマりしました。
きっついですね。
辞書のライセンス
SKKの辞書ファイルはGPLで配布されています。 フリーなソフトウェア!! 自由!!
...こういうデータファイルがGPLっていうのはどういう扱いなんでしょう。
wikipediaから引用すると、GPLはおおむね以下のことを許諾するライセンスです。
- プログラムの実行
- プログラムの動作を調べ、それを改変すること(ソースコードへのアクセスは、その前提になる)
- 複製物の再頒布
- プログラムを改良し、改良を公衆にリリースする権利(ソースコードへのアクセスは、その前提になる)
改変の許可と再配布はいいとして、実行というのは何になるんでしょうね。 あとテキストを読み取ってアレコレするのは動的リンク扱いで派生物扱いになったりするんでしょうか。
謎です。
AquaSKK/FlickSKKのメンテナとしてのポジショントークは、同梱であってリンクではない。そのためGPLである辞書ファイルを読み取ってあれこれするプログラムはGPLである必要性はない、です。
別にアンチGPLというわけではなくて、AppleのAppStoreとGPLの相性が悪いので、俺の書いたコードはGPLではない、ということにしとかないと色々と都合が悪いんです。
まとめ
4コマ漫画の画像管理✨
NEW GAME! のコマ検索 - みずぴー日記で作っていたソフトウェアのうち、画像以外の部分を公開した。
レポジトリ
https://github.com/mzp/EagleJumpSystem
検索機能
NEW GAME! のコマ検索 - みずぴー日記で述べたように
- 台詞
- 登場キャラクター
によって該当するコマを検索できる。
入力補助機能
検索機能を実現するために、各コマにはメタデータ(台詞、登場キャラクター)を入力する必要がある。 このメタデータの入力を補助する機能もいくつか実装した。
コマ分割
ページを取り込む際に、画像をコマごとに分割する。これはゆゆ式を無限に楽しみたかった話 〜 ゆゆ式 Advent Calendar 2014 20日目 〜 - non117's diaryのツールをほぼそのまま利用している。
テキストの自動認識
Google CloudVisionAPIによりコマ中のテキストを自動で認識する。
ただし利用には別途APIキーの取得が必要である。APIキーの取得方法はCloud Vision APIの使い方まとめ (サンプルコード付き)が分かりやすかった。
キャラクターの半自動認識
ある程度の量のキャラクターの分類を手動で行なえば、残りの画像については機械学習により自動でタグづけが行なえる。
ただし、登場回数の少ないキャラクターは学習データが少ないので、そこは手動でタグをつける必要がある。また、顔認識をした上でキャラクターの分類をしているため、「手のみ登場している」といったコマについては対応できない。
この部分のコードはTensorFlowでアニメゆるゆりの制作会社を識別する - kivantium活動日記をかなり参考にしている。
その他
既知のバグ
もしくは面倒で直してない箇所リスト。
- コマごとの分割の際に、まれに0バイトの画像が生成される。 今は、定期的に手動で
find . -size 0 -exec rm {} \;
を実行して削除しているが、そもそも生成されないようにしたい。 - 素朴なデータの格納方法をしているため、画像が増えるとどんどん遅くなっていく。
名前
レポジトリ名のEagleJumpSystemはNEW GAME!に登場するイーグルジャンプ社に由来する。
📱Zoi for iPhone
NEW GAME! のコマ検索 - みずぴー日記の成果を用いて、iPhoneからNEW GAME!のコマ検索を行なえるようにした。
経緯
ニコニコでやっているアニメの一挙放送を見ると、PCの前に4〜6時間ほど拘束される。 そのときに、なんとなくXcodeを立ちあげてコードを書きはじめてしまった。
桜Trick、一挙放送みながらiOSアプリつくってたら、だいぶ便利な感じがでてきた。 pic.twitter.com/UIzmPc98DF
— mzp (@mzp) February 21, 2016
機能
コマのインクリメンタル検索
NEW GAME! のコマ検索 - みずぴー日記に各コマのセリフを入力済みなので、それを移植してインクリメンタルに検索できるようにした。
またUIActivityViewControllerを用いたアプリ間連携も行なえる。
Spotlight対応
App Searchに対応したため、Spotlightから検索ができる。
Spotlightのインデックスに登録するタイミングは、悩んだが、
- 初回起動時
- 明示的に再インデックスを指示した時
の2つにした。 最初は、初回起動時のときのみにしていたが、インデックス処理でエンバグしたときの対応が面倒だったので、再インデックスボタンを追加した。
インデックスへの追加はApp Search プログラミングガイド: 検索の基本にあるコードをほぼそのまま使っている。
func add(items : [ZoiJson.Item], complete: () -> ()) { let si = items.map { self.searchableItemFor($0) } CSSearchableIndex.defaultSearchableIndex().indexSearchableItems(si) { error in if error != nil { print(error?.localizedDescription) } else { complete() } } private func searchableItemFor(item : ZoiJson.Item) -> CSSearchableItem { let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypePNG as String) attributeSet.title = item.script if let image = ZoiImage.image(item) { attributeSet.thumbnailData = UIImagePNGRepresentation(image) } return CSSearchableItem(uniqueIdentifier: item.path, domainIdentifier: "zoi", attributeSet: attributeSet) }
newgame:// 対応
ぐらばくさんが提案していた、newgame:// をカスタムURLスキームとして取り込んだ。(参考: ぐらばく on Twitter: "newgame://3/65/2/2 もいい https://t.co/hC6t3KxYj0")
https://twitter.com/mzp/status/702863419226005505
newgame://巻数/ページ番号/列/行
というフォーマットだが、目次等の存在により書籍のページ番号と先頭からページ数は一致しないので、そこは補正した。 また、ページ内の何コマ目なのかは保持しているが、何列目かは保持していないので、そこの補正も行なっている。
func toPath(url : NSURL) -> String? { if let vol = url.host, let paths = url.pathComponents, let page = Int(paths[1]), let col = Int(paths[2]), let row = Int(paths[3]) { let pageStr = NSString(format: "%03d", page + 2) let pos = (col - 1) * 3 + row return "data/vol\(vol)/vol\(vol)_\(pageStr)_\(pos).jpeg" } else { return nil } }
OnDemandResource対応
Testflightにアップロードしようとしたが100MBを越えてしまった。そこで、OnDemandResourceを利用し、必要になったタイミングで画像を取得するようにした。
OnDemandResourceの使い方はオンデマンドリソースでiOSアプリを軽くする - Qiitaを参考にした。
またOnDemandResourceで取得した画像を、ImageViewに非同期で設定するために https://github.com/Haneke/HanekeSwiftを用いてる。標準ではOnDemandResourceからの取得はできなかったので、独自のFetcherを作った。
import Foundation import Haneke class OnDemandResource { func fetch(fail : NSError? -> Void, succeed : Void -> Void) { let request = NSBundleResourceRequest(tags: Set(arrayLiteral: "zoi")) request.conditionallyBeginAccessingResourcesWithCompletionHandler() { resourceAvailable in if resourceAvailable { succeed() } else { request.beginAccessingResourcesWithCompletionHandler() { error in if error == nil { succeed() } else { fail(error) } } } } } } class OnDemandFetcher<T : DataConvertible> : Fetcher<T> { private let resource = OnDemandResource() private let getValue : () -> T.Result? init(key: String, @autoclosure(escaping) value getValue : () -> T.Result?) { self.getValue = getValue super.init(key: key) } override func fetch(failure fail: ((NSError?) -> ()), success succeed: (T.Result) -> ()) { resource.fetch(fail) { if let result = self.getValue() { self.main { succeed(result) } } else { self.main { fail(nil) } } } } override func cancelFetch() {} private func main(f : () -> ()) { dispatch_async(dispatch_get_main_queue(), f) } } // 使い方 let fetcher = OnDemandFetcher<UIImage>(key: self.item.path, value: UIImage(named: "zoi.png")) self.image.hnk_setImageFromFetcher(fetcher, placeholder: UIImage(named: "placeholder.jpeg"), format: Format<UIImage>(name: "original"))