Tag Archive for 'hack'

SafariでMigemoを使えるようにしようとした

SIMBLプラグインをつくる習作として、Safariのページ内検索にMigemoをつかえるようにできないか試行錯誤してみました。 分かったことは、SIMBLプラグインとしてつくるのは難しそう、ということでした。

作戦

Migemoの出力は正規表現です。

例: takai → (タカイ|タカイ|隆一|多階層|他界|高[市石鼾泉井]|たかい|takai|takai)

対して、Safariのページ内検索は1つのキーワードに対する全文検索です。 つまり、探すべきキーワードが1つから複数に変わるということです。これは大きな差。

ならば、元のコードでキーワードを渡して検索している関数を、キーワードの個数ぶんだけ繰り返し呼べば所望の機能が実現できそうだ、と考えました。

どこに手を入れるか

Safariの文字列検索を追う で書いていたように、WebView::searchFor にブレークポイントをはり、デバッガでステップ実行して挙動を調べました。

コードを書き変える

まず、元の searachFor メソッドを _safari_searchFor メソッドに変名してとっておき、 この_safari_searchFor メソッドをMigemoの正規表現から得られる文字列の数だけ呼び出す、という戦略を考えます。 うまくいけば、SIMBLプラグインとして実装する際に Method Swizzling として実現できますね。

ためしに、与えられたキーワードと google の2つで検索するようにしてみました。

Index: mac/WebView/WebViewPrivate.h
===================================================================
--- mac/WebView/WebViewPrivate.h        (revision 32849)
+++ mac/WebView/WebViewPrivate.h        (working copy)
@@ -106,6 +106,9 @@
  */
 - (BOOL)searchFor:(NSString *)string direction:(BOOL)forward caseSensitive:(BOOL)caseFlag wrap:(BOOL)wrapFlag startInSelection:(BOOL)startInSelection;
 
+- (BOOL)_safari_searchFor:(NSString *)string direction:(BOOL)forward caseSensitive:(BOOL)caseFlag wrap:(BOOL)wrapFlag startInSelection:(BOOL)startInSelection;
+
+
 - (void)setMainFrameDocumentReady:(BOOL)mainFrameDocumentReady;
 - (void)setTabKeyCyclesThroughElements:(BOOL)cyclesElements;
Index: mac/WebView/WebView.mm
===================================================================
--- mac/WebView/WebView.mm      (revision 32849)
+++ mac/WebView/WebView.mm      (working copy)
@@ -3002,6 +3002,14 @@
 
 - (BOOL)searchFor:(NSString *)string direction:(BOOL)forward caseSensitive:(BOOL)caseFlag wrap:(BOOL)wrapFlag startInSelection:(BOOL)startInSelection
 {
+  BOOL ret1 = [self _safari_searchFor:@"google" direction:forward caseSensitive:caseFlag wrap:wrapFlag startInSelection:startInSelection];
+  BOOL ret2 = [self _safari_searchFor:string direction:forward caseSensitive:caseFlag wrap:wrapFlag startInSelection:startInSelection];
+
+  return ret1 and ret2;
+}
+
+- (BOOL)_safari_searchFor:(NSString *)string direction:(BOOL)forward caseSensitive:(BOOL)caseFlag wrap:(BOOL)wrapFlag startInSelection:(BOOL)startInSelection
+{
  if (_private->closed)
    return NO;
 
@@ -3247,24 +3255,27 @@
 
 - (NSUInteger)markAllMatchesForText:(NSString *)string caseSensitive:(BOOL)caseFlag highlight:(BOOL)highlight limit:(NSUInteger)limit
 {
-    WebFrame *frame = [self mainFrame];+    NSString *strs[2] = {@"google", string};
     unsigned matchCount = 0;
-    do {
-        id <WebDocumentView> view = [[frame frameView] documentView];
-        if ([view conformsToProtocol:@protocol(WebMultipleTextMatches)]) {
-            [(NSView <WebMultipleTextMatches>*)view  setMarkedTextMatchesAreHighlighted:highlight];
-            ASSERT(limit == 0 || matchCount < limit);
-            matchCount += [(NSView <WebMultipleTextMatches>*)view markAllMatchesForText:string caseSensitive:caseFlag limit:limit == 0 ? 0 : limit - matchCount];
 
-            // Stop looking if we've reached the limit. A limit of 0 means no limit.
-            if (limit > 0 && matchCount >= limit)
-                break;
-        }
-        
-        frame = incrementFrame(frame, YES, NO);
-    } while (frame);
-
+    for (int i(0); i<2; i++) {
+        WebFrame *frame = [self mainFrame];
+        do {
+            id <WebDocumentView> view = [[frame frameView] documentView];
+            if ([view conformsToProtocol:@protocol(WebMultipleTextMatches)]) {
+                [(NSView <WebMultipleTextMatches>*)view  setMarkedTextMatchesAreHighlighted:highlight];
+                ASSERT(limit == 0 || matchCount < limit);
+                matchCount += [(NSView <WebMultipleTextMatches>*)view markAllMatchesForText:strs[i] caseSensitive:caseFlag limit:limit == 0 ? 0 : limit - matchCount];
+                
+                // Stop looking if we've reached the limit. A limit of 0 means no limit.
+                if (limit > 0 && matchCount >= limit)
+                    break;
+            }
+            
+            frame = incrementFrame(frame, YES, NO);
+        } while (frame);
+    }
     return matchCount;
 }

じっさいに、マッチしたキーワードをハイライト表示させているのは markAllMatchesForText というメソッドのようだったので、 こちらにも複数キーワード対応するためのハックを仕込んでおきます。

実行結果

result

複数キーワードがハイライト表示されました。やりましたね :) ただし、「次の候補へ」のようにして、次のマッチ文字列に飛ぼうとしても、同じキーワード間でしかジャンプできませんでした。 ><

原因と対策

そもそも、キーワードにマッチした元のテキストやマッチした結果はどこに保存されているのか。 これを調べていくと WebCore の中に入っていくことになります。 WebCoreはKHTML由来のコードで、C++で書かれているライブラリです。 となると、Objective-C みたいに実行時に柔軟なメソッド差し替えはできなくなってしまいます。 今回はSIMBLプラグインの習作が目的だったので、深追いはここまで。

本気でやるならば、C/Migemoと鬼車をWebCoreにリンクして、マッチしたテキストの範囲などを複数キーワードに対応させればよいでしょう。

まとめ

あと少しだったんだけどなあ…

手駒

  • Migemoの実装には、C/Migemoをつかいます。
  • Migemoの出力は正規表現なので、鬼車のCocoa版であるOgreKitをつかいます。
  • Safariをハックするには、WebKitをデバッグビルドするのが便利です。
  • Cocoaアプリを拡張するには、SIMBLプラグインをつくるのが定石です。

Safariの文字列検索を追う

Safariで、ページ内検索がどのように行われているかを、ちょっと追ってみました。以下かんたんなメモ。

準備

Safari.appはリリースビルドなので、WebKit をつかいます。 Debugging WebKit の手順どおりやると、デバッグシンボルのついたWebKitでSafariがつかえるようになります。

set breakpoint

search というキーワードで、WebKit の中を検索してみたところ、いくつかひっかかります。 WebView:searchFor というのが怪しいんじゃないか、と勘であたりをつけ、ブレークポイントを張り、デバッグ実行 (Cmd-Y)。

実際に、Cmd-F してページ内検索を実行し、ブレークポイントにひっかかったところ: break

まんまとひっかかりましたね。

最終的には、WebCore::CircularSearchBuffer::isMatch という関数内で文字列の比較がmemcmpで行われています。

まとめ

ソースを静的に解析してブレークポイントにあたりをつけ、実行時の挙動を見てハックするポイントをさがす、という例でした。 次は、じっさいにこのあたりに手を入れてみます。

夏ライオンのおれおれビルド

Twitterクライアントの夏ライオンを、きわめて自分好みに改造しました。作者の @akrさん 曰く、どんどん横流ししておk だそうなので、ここで公開しておくことにしました。

ダウンロード

夏ライオンに従って、修正BSDライセンスです。

なにがちがうか

その1: 詰め込み表示

以下の画像の左がオリジナル、右がここで公開してるもの。

tweaked

いっぺんに表示できるメッセージを増やして、より世界の風を感じることができるようになっています。

その2: メッセージフィルタリング

夏ライオンにメッセージフィルタリングをつけるハック で書いたパッチを適用しています。


ほんと、ソースが公開されてるってすばらしいですね! akr++。

夏ライオンにメッセージフィルタリングをつけるハック

@akrさんがつくっている、夏ライオンというTwitterクライアントに、表示されるメッセージを特定の条件でフィルタリングするハックをしてみました。

何ができるか

特定のユーザのメッセージだけを表示したり、

ある言葉が含まれるメッセージだけを表示したりできます。

キモ

  • どこから手をつけるか
  • NSPredicate をつかう

長くなるので、続きは以下で。

Continue reading ‘夏ライオンにメッセージフィルタリングをつけるハック’

Ticket2RTM

A Trac plugin that synchronizes Trac tickets and Remember the Milk tasks.

  • when a ticket is created, a RTM task also will be added.
  • when you close a ticket, the associated RTM task will be completed.

Screenshot

Screencast

Download

The source code is on github: http://github.com/mootoh/ticket2rtm/

Benefits

  • unified Task List
  • check tickets from mobile

Install

build

  1. edit Makefile and set PLUGINS_DIR to your Trac plugin dir
  2. make

or:

  1. python setup.py –bdist_egg
  2. copy dist/TracTicket2RTMPlugin*.egg into your Trac plugin dir

RTM API key, token

This plugin requires RTM API key, shared secret, and write-permitted token. You can obtain your API key and shared secret from here.

After acquired key and shared secret, you can get your token by using bundled script. Run as follow:

% python get_token.py <api key> <shared secret>

Configuration

  1. copy/paste contents of trac_ini_sample.txt to your trac.ini
  2. set rtm_api_key, rtm_api_secret, rtm_api_token to ones acquired above
  3. enjoy !

Ticket2RTM 0.1

TracのチケットからRemember the Milkのタスクを自動生成するTracプラグインをつくりました。 詳しくは trac:Ticket2RTMに。

スクリーンショット

チケットがつくられたら、自動的にRTMのタスクがつくられます。

スクリーンキャスト

ダウンロード

ticket2rtm-0.1.zip

動機

やることリストを1つにしたいと思ったのでした。

  • RTMで日々のやることを管理しつつ、何かつくるときはTracでチケットを切ってる → タスクがあちこちに分散
  • 複数のプロジェクトを同時にTracですすめる → チケットがあちこちにありすぎ

あちこちにタスクが散らばっていては、やらなきゃいけないことを見落としてしまったり、やらないことを決めるためのコストが増えてしまったりで、ストレスフリーな状態になかなかなれません。

ぜんぶまとめて1つのインターフェイスで見られたら幸せだろうな、RTMのインターフェイスはいいよな、と考えてプロトタイプをつくってみたのでした。

あと、RTMはモバイル版もあるので、チケットをケータイからも確認できるという副作用的メリットもあります。

学んだこと

Python

Pythonのコードをまともに書くのは始めてでした。selfうっとうしいとか、正規表現めんどいとか、インタラクティブ環境でのreloadを最初しらなくてえらい非効率だったりとか、switchないの? とか驚きの連続でしたが、なんとかなるもんですね。 プラグインをつくるためにTracのコードを読んでたのですが、インデント強制な言語はコードリーディングにいいですね。 ものの見方を増やせたのはよかったかなと。

RTM連携させるために、RTM APIのPythonラッパの一部を書くことになりました。 これには1年ほど前にrtmilk.rbというRuby版をつくっていた経験が活きました。多少言語のsyntaxが変わろうが、やりたいことがはっきりしてれば考え方は同じ。

Trac Plugin

TracでPluginを書くのは思っていたよりも簡単でした。 これをとっかかりにして、いろいろなアイデアを形にしていこうかなと。


今年のアウトプット第一弾はTrac+RTMハックでした。 1/14から始めて、夜なべでつくり、1/18に0.1リリース。思いつき駆動だなー。 しかしこれで明日のShibuya.trac ミーティング1.0、明後日のRemember The Milk 交流会 in 東京で話せるネタができたということで。

2007につくったもの

2007のおすすめ記事x3を書きながら、 2007年ももう終わりだし、今年1年でつくったものをまとめておくことにしました。 以下時系列で。

16個ですね。ちょっとしたものを思いついてはちゃちゃっとつくる、という姿が現れてます。 RTM → QuartzComposer → Twitter → Quicksilver と興味が移ってきているのが分かりますね。

来年は、もっと人の役に立つものをしっかりこつこつとつくっていこう。

QSTwitter 1.1 (Quicksilver Twitter Plugin)

QSTwitter (Quicksilver Twitter Plugin) 1.1を公開します。

QSTwitter.zip をダウンロードしてダブルクリック!

変更点は、Twitterエントリの”from=…”のところに、”QSTwitter”が入る、というものです。ユーザーエージェントみたいなものですね。


おまけ : どうやって自作Twitterアプリでfrom=”…”に自分のアプリ名を入れるか

  • Twitterの中の人にメールを送る
  • 自分アプリで投稿するときに “source=YourAppName“というパラメータを追加
  • Twitter側の対応を待つ

でした。 僕の場合は、10日ほどで対応してもらえました。Twitterの中の人に感謝!

参考 : Twitter Development Talk::”from Web”

はてなハイクに投稿するシェルスクリプト

はてなハイクで遊び始めました。

コード

#!/bin/sh
RKM='REPLACE WITH YOUR RKM'
KEYWORD=$1
BODY=$2
 
curl http://h.hatena.ne.jp/entry 
  -d rkm=$RKM 
  -d "word=$KEYWORD" 
  -d "body=$BODY" 
  -b cookies.txt

実行例

% ./haiku.sh Test "test post from haiku.sh !"

メモ

  • ASCIIなキーワード、本文だけ
  • cookies.txtはFirefoxのprofileから、はてなでログイン済みのものをコピーしてきました

RKMの値は、はてなハイクのソースをブラウザから見て

input type=”hidden” name=”rkm” value=”xxxxxx”

のxxxxxxの部分にある文字列をコピペるとOKでした。


http://h.hatena.ne.jp/keyword/はいくハックでいろいろやってます。

Quicksilverプラグインのつくりかた

Quicksilverでプラグインをつくる方法をチュートリアルにしてまとめました。

このチュートリアルを読むことで、以下のものがつくれるようになります。

  • 基本的なQuicksilverプラグイン
  • プラグイン用の設定画面
  • 設定画面からのデータ取得

目次

  • ここでの環境
  • 必要なもの
  • 準備
  • つくりはじめる
  • PreferencePaneをつくる
  • Actionのコードを書く
  • おわりに
  • 参照

ここでの環境

  • Mac OS X Leopard 10.5.1
  • Quicksilver B53 (3814)
  • Xcode 3.0

必要なもの

準備

ビルドできるようになるまでの準備をします。

1. ダウンロードしたXcodeのテンプレートをインストール

Xcode 3.0 からは、カスタムテンプレートを置く場所が /Developer/Library/Xcode/Project Templates/ になっています。 (VIRTUOSO VIRTUOSOのところでは、 /Library/Application Support/Apple/Developer Tools/Project Templates/ に置くよう書かれている)

ここでは、 /Developer/Library/Xcode/Project Templates/Bundle/Quicksilver Plug-in というパスになるように、zipを展開したディレクトリを置きます。

2. Xcode プロジェクトをつくる

1がうまくいっていれば、このように Quicksilver Plug-in が選択できます。

すすめると、こんな画面に。

このチュートリアルでは、設定画面 (PreferencePane) もつくるので、 QSInterface.frameworkにもチェックして、リンクされるようにしておきましょう。

3. Source Trees

プラグインをビルドするには、Quicksilverのフレームワークが必要です。 ダウンロードしておいたQuicksilver Developer版の中に、フレームワークがあるのでそこをXcodeに指定します。

XcodeのPreferencesを開き、Source Treesタブに移動して、画面のように設定します (Pathのところは、Quicksilver Developer版を置いた場所に読み替えてください。 例:/Applications/QS-dev.app/Contents/Frameworks)

つくりはじめる

フレームワークにのっかってビルドできるところまでいってみましょう。

4. Info.plist

さて、ここまでで準備が完了しました。 ここからは実際にプラグインをつくっていくことになります。

プラグインの情報のほとんどは、Info.plistに書くことになります。 .plistなファイルはXMLなので、Property List Editorで開くと編集にラクができます。

デフォルトのInfo.plistをProperty List Editorで開くと、次のような感じ。

個々のキー/値については、 Anatomy of a plugin – Info.plist Part 1Plist Specification が詳しいです。 ここでは、最小限の設定項目を埋めていきます。

5. QSPlugin

プラグインについてのメタ情報を書きます。

Before | After:

6. QSActions

実際にプラグインが何をするかについての情報を書きます。 まず、QSActionsTemplateからQSActionsに名前を変えておきます。

nameのところの値をタイプすることで、Quicksilverからこのプラグインが呼ばれるので、 それっぽい値を入れておくとよいでしょう。

directTypesのところで、受けつける入力の種類を判定するぽいです。 ここでは “*”を指定してなんでもこいにしてみました。

Before | After:

7. QSRegistration

いろいろ設定項目はありますが、ここではPreferencePaneについて。 まず、QSRegistrationTemplateからQSRegistrationに名前を変えておきます。

QSPreferencePanesの中に、classを指定できるところがあります。 ここには、後でつくるPreferencePane用のクラス名を指定します。

Before | After:

8. ビルド

そろそろこのページを見ながらちまちまと作業をするのにも飽きてきたことででしょう。 ここらでビルドして、実際に動いてるのを見ようではないですか。

Xcodeでふつうにビルドすると、*.qsplugin というファイルが生成されます。 これがQuicksilverのプラグインなのですね。

ダブルクリックすると、Quicksilverに “プラグインをインストールする? “みたいに聞かれるのでYesで進みましょう。

9. 動いてるのを見る

「.」でテキスト入力モードにして何かうちこみ、TABでActionに移りましょう。 6.で設定してある名前を入力すればほら、いまつくったプラグインが呼び出せるではないですか! (とはいえ、いまのところ何もActionを実装していないので、実行したところで何も起こりません)

次に、QuicksilverのPreferenceを開いてみます。 左のリストに、いまつくったプラグインがあると思います。やったね !

PreferencePaneをつくる

ここからがお楽しみです。

10. PreferencePaneのクラスをつくる

File→Newから、Objective-C class をつくります。 ファイル名は、7で指定してたクラス名にするとよいでしょう。

@interfaceを、QSPreferencePaneを継承するように書き換えます。

QSPluginTutorialPreferencePane.h:

#import <qsinterface/QSPreferencePane.h>
 
@interface QSPluginTutorialPreferencePane : QSPreferencePane {
}
 
@end

@implementationに、mainNibNameというメソッドを加えます。 このメソッドは、次につくるnibファイルの名前 (から.nibを除いたもの) を返すようにします。

QSPluginTutorialPreferencePane.m:

#import "QSPluginTutorialPreferencePane.h"
 
 
@implementation QSPluginTutorialPreferencePane
 
-(NSString *)mainNibName {
	return @"QSPluginTutorialPreference";
}
 
@end

ここまでのプロジェクトはこんな感じになるでしょう。

11. NIBをつくる

File→Newから、Window NIB をつくります。

12. File’s OwnerのクラスをPreferencePaneのクラスにする

このへんを 参考にして、File’s Ownerのクラスを、10でつくったクラスにします。

まず、QSPreferencePane.hと10でつくったクラスのヘッダファイルを、 NIBのウィンドウにドラッグ&ドロップします。

次に、File’s Ownerのオブジェクトを選択した状態でObject Identityの設定ウィンドウに行き、 Classを10でつくったクラス名にします。

13. File’s OwnerとWindowをつなげる

File’s OwnerのオブジェクトからCtrl+ドラッグしてWindowにドロップすると、 オブジェクトをつなげることができます。 Outlets _windowと出るので、_windowを選択しましょう。

14. コントロールを配置

好きなコントロールをWindowにつけましょう。 ここでは、シンプルなText Fieldコントロールをつけました。

15. 動かす

ふたたびXcodeでビルドし、.qspluginをつくり、インストールします。 QuicksilverのPreference→つくっているプラグインを選ぶと、ちゃんと右側にいまつくったText Fieldが 表示されることでしょう。やった !

16. バインドする

しかしこのままでは、設定画面で入力した値をどうやって使うのか分かりませんよね。 ここで、Cocoa Binding というテクニックをつかいます。

14で配置したコントロールを選択してBindngs設定ウィンドウに行き、次のように設定します。

  • Bind to: Shared User Defaults
  • Controller key: values
  • Model Key Path: 好きな文字列 (他のところからこの値を参照するときに使うキー)

ここでは、Model Key Path を QSPluginTutorial.someKey という値にしました。

Actionのコードを書く

したいことを何でも書くところ。

17. 入力 + 設定から得た値 => 出力

Actionのクラスの実装を書きます。 ここでは、入力された文字列に、PreferencePaneから得た値を組合せて、出力にすることにしました。 コードは以下のようになります。

QSPluginTutorialAction.m:

@implementation QSPluginTutorialAction
 
- (QSObject *)performActionOnObject:(QSObject *)dObject{
    // get value from Preference Pane
    id values = [[NSUserDefaultsController sharedUserDefaultsController] values];
    NSString *value = [values valueForKey:@"QSPluginTutorial.someKey"];
 
    QSObject *ret = [QSObject objectWithString:
      [NSString stringWithFormat:@"%@ hello, %@",[dObject stringValue], value]];
 
    return ret;
}
@end

18. 完成 !!!

長い道のりでしたが、ついにこれで終わりです。

ビルドし、インストールして、PreferencePaneから値を入れておきます。

テキストを入力し、Actionを選択して実行します。

もういちどQuicksilverを起動すると、入力のところに結果が入っています。やったね !

おわりに

Quicksilverでプラグインをつくる方法をチュートリアルにしてまとめました。

Quicksilver Twitter Plugin (日本語) で、Quicksilverのプラグインをつくっているとき、まとまった情報がない、情報が古い、日本語の情報が見当たらない、と何かと苦労してました。 このチュートリアルでQuicksilver野良プラグインを書く人が増えればいいなあと。 (そして素晴しいプラグインが増えて僕も幸せになれればいいなぁとw)

Catalogの扱い方とかまだ調べてみたいところがたくさんあるので、情報持ってる方いればぜひご連絡ください。

参照

Vacuous Virtuoso: ほぼ唯一のQuicksilverプラグインづくりの情報源。 基本はここを以下の順に読むとよいです。

docs.blacktree.com: 本家。でもドキュメントが散逸してます…