「http://d.hatena.ne.jp/Sybian/20070415/p1」をGreasemonkeyに移植した。

インストール

http://sawat.jf.land.to/gm/google_ldr_keybind_fx.user.js
(追記) 本家がFirefox/Opera両対応になったので、こちらをインストールしてください。
http://sybian.picopico.cc/data/google_ldr_keybind_fx_op.user.js

ソース

// ==UserScript==
// @name        Google Search keybind as a LDR
// @namespace   http://d.hatena.ne.jp/Sybian/
// @include     http://*.google.co*/search?*
// @description Google keybind as a LDR
// ==/UserScript==


// keybinds are written bottom. 

function log(str) {
//  unsafeWindow.console.log(str);
}

(function(){
	// for popup block through..?
	var wo=window.open;
	window.openLater=function(){
		var args=arguments;
		setTimeout(
			function(){
				wo.apply(window,args)
			},100
		);
	}


	//**************************************************
	// css
	//**************************************************
	function CSS(){
		this._elm=[];
		this._cnt=0;
		this._hash={};
		this._head=document.getElementsByTagName("head")[0];
	}
	CSS.prototype.factory=function(id){
		this._cnt++;
		var id=id || this._cnt;
		var elm=document.createElement("style");
		elm.type="text/css";
		elm.id="css_"+id;
		return elm;
	}
	CSS.prototype.toggle=function(selector,prototype,id){
		if(!id) return;
		if(this._hash["css_"+id]){
			this.removeRule(id);
		}else{
			this.addRule(selector,prototype,id);
		}
	}
	CSS.prototype.addRule=function(selector,property,id){
		if(this._hash["css_"+id]) return ;
		var elm=this.factory(id);
		elm.innerHTML=selector+"{"+property+"}";
		this._head.appendChild(elm);
		this._hash[elm.id]=true;
		return elm.id;
	}
	CSS.prototype.removeRule=function(id){
		var elm=document.getElementById("css_"+id);
		if(elm){
			this._head.removeChild(elm);
			this._hash["css_"+id]=false;
		}
	}
	var css=new CSS();

	//**************************************************
	// panel
	//**************************************************
	var panel=document.createElement("div");
	panel.id="ldr_panel";
	css.addRule(
		"#ldr_panel"
		,[
			,"position:fixed"
			,"right:10px; top:10px;"
			,"background:#eee;"
		].join(";")
		,"panel"
	);
	css.addRule(
		"div.g.pinned"
		,[
			"background:#e9ffe9"
		].join("")
		,"pinned"
	);
	css.addRule(
		"nobr .pinned"
		,[
			"background:#3f3;"
		].join("")
		,"pinned_cache"
	);

	//--------------------------------------------------
	// panel list
	//--------------------------------------------------
	panel.list={
		parent: panel
		,element: document.createElement("ol")
	}
	panel.list.element.style.display="none";
	panel.list.parent=panel;
	panel.list.update=function(){
		var tmp=[];
		for(var i=0,l=this.parent.pin._pins.length; i<l; i++){
			var pin=this.parent.pin._pins[i];
			tmp.push([
				'<li>'
				,'<a href="',pin.url,'">',pin.title,'</a>'
				,'</li>'
			].join(""));
		}
		this.element.innerHTML=tmp.join("");
	}
	panel.list.display=function(){
		this.element.style.display="block";
	}
	panel.list.hidden=function(){
		this.element.style.display="none";
	}
	panel.list.toggle=function(){
		if(this.element.style.display == "none"){
			this.display();
		}else{
			this.hidden();
		}
	}

	//--------------------------------------------------
	// panel pin
	//--------------------------------------------------
	panel.pin={
		parent: panel
		,_pins: []
		,_hashTable: {}
	};
	panel.pin.element=document.createElement("p");
	panel.pin.hash=function(url){
		return url.replace(/[^a-zA-Z0-9-_]/g,"");
	}
	panel.pin.update=function(){
		this.element.innerHTML=this._pins.length;
		this.parent.list.update();
	}
	panel.pin.toggle=function(url, title, element){
		log("toggle >> "+ this._hashTable[this.hash(url)]);
		if(this._hashTable[this.hash(url)]){
			this.remove(url, title, element);
		}else{
			this.add(url, title, element);
		}
	}
	panel.pin.add=function(url,title,element){
		if(!url) return ;
		var key=this.hash(url);
		if(this._hashTable[key]) return;
		this._hashTable[key]=true;
		this._pins.push({url:url,title:title,element:element});
		element.className+=" pinned";
		this.update();
	}
	panel.pin.remove=function(url,title,element){
		var key=this.hash(url);
		if(!this._hashTable[key]) return;
		for(var i=0,l=this._pins.length; i<l; i++){
			if(this._pins[i].url == url){
				this._pins.splice(i,1);
				this._hashTable[key]=false;
				element.className=element.className.replace(/ pinned/g,"");
				this.update();
				break;
			}
		}
	}
	panel.pin.clear=function(){
		var i=0;
		while(this._pins[i]){
			var pin=this._pins[i];
			this.remove(pin.url,pin.title,pin.element);
		}
		this.update();
	}
	panel.pin.openAll=function(){
		for(var i=0,l=this._pins.length; i<l; i++){
			window.openLater(this._pins[i].url);
		}
		this.clear();
	}
	panel.pin.open=function(num){
		window.open(this._pins[num].url);
		this.remove(this._pins[num].url);
	}


	//--------------------------------------------------
	// Items
	//--------------------------------------------------
	function Items(){
		this._items=[];
		this.current=-1;
		this.allItems();
	};
	Items.prototype.allItems=function(){
		if(this._items.length > 0) return this._items;
		var div=document.getElementsByTagName("div");
		for(var i=1,l=div.length; i<l; i++){
			if(div[i].className == "g"){
				div[i].id="item_"+(i-2);
				this._items.push(div[i]);
			}
		}
		return this._items;
	}
	Items.prototype.focus=function(){
		location.hash="item_"+(this.current);
//		location.hash="";
	}
	Items.prototype.prev=function(){
		if(this.current > 0) this.current--;
		this.focus();
	}
	Items.prototype.next=function(){
		if(this.current < this._items.length) this.current++;
		this.focus();
	}
	Items.prototype.top=function(){
		this.current=0;
		this.focus();
	}
	Items.prototype.bottom=function(){
		this.current=this._items.length-1;
		this.focus();
	}
	Items.prototype.getItem=function(){
		var item=this._items[this.current];
		try{
			var _a=item.getElementsByTagName("h2")[0].getElementsByTagName("a")[0];
			var url=_a.href;
			var title=_a.innerHTML;
		}catch(e){
			log(e);
		}
		try{
		var cache=item.getElementsByTagName("nobr")[0].getElementsByTagName("a")[0] || {};
		var cacheURL=cache.href || "";
		}catch(e){
			log(e);
		}
		return {
			element: item || {}
			,url: url || ""
			,title: title || ""
			,cache: cacheURL || ""
			,cacheElement: cache
		};
	}
	var items=new Items();


	//--------------------------------------------------
	// keybind
	//--------------------------------------------------
	var keybind={
	}
	keybind.keycode2char=function(e){
		var kc=e.keyCode;
		// from LDR(event.js)
		var between = function(a,b){
			return a <= kc && kc <= b
		}
		log(kc);
		var _32_40 = "space pageup pagedown end home left up right down".split(" ");
		var kt = {
			8  : "back",
			9  : "tab",
			10 : "enter",
			13 : "enter",
			16 : "shift",
			17 : "ctrl",
			44 : ",",
			46 : ".",
			47 : "/",
			58 : ":", // keypress
			60 : "<", // keypress
			62 : ">", // keypress
			63 : "?", // keypress
			91 : "[",
			93 : "]",
			
			188: "<",
			190: ">",
			219: "[",
			221: "]",

			229: "IME"
		};
		var result=(function(){return (
			between(65,90)  ? String.fromCharCode(kc+32) : // keydown  a-z
			between(97,122) ? String.fromCharCode(kc) :    // keypress a-z
			between(48,57)  ? String.fromCharCode(kc) :    // 0-9
			between(96,105) ? String.fromCharCode(kc-48) : // num 0-9
			between(32,40)  ? _32_40[kc-32] :
			kt.hasOwnProperty(kc) ? kt[kc] : 
			"null"
		)})();
		result=(e.shiftKey) ? "S-"+result : result;
		result=(e.ctrlKey)? "C-"+result : result;
		//alert(result + "/" + kc);
		return result;
	};
	keybind.callback=function(key,func){
		var func=func || function(){};
		func();
	}

	//**************************************************
	// event
	//**************************************************
	function eh(e){
		var ch=keybind.keycode2char(e);
		log(">> " + ch);
		if(keybinds[ch]){
			keybind.callback(ch,keybinds[ch]);
			e.preventDefault();
		}
	}
	function EventHandler(){
		this._enabled=false;
	}
	EventHandler.prototype.toggle=function(){
		if(this._enabled){
			this.disable();
		}else{
			this.enable();
		}
	}
	EventHandler.prototype.enable=function(e){
		log("enable");
		if(this._enabled == false){
			document.getElementsByTagName("html")[0].addEventListener("keydown",eh,true);
			this._enabled=true;
		}
	}
	EventHandler.prototype.disable=function(){
		log("disable");
		if(this._enabled == true){
			document.getElementsByTagName("html")[0].removeEventListener("keydown",eh,true);
			this._enabled=false;
		}
	}

	var ev=new EventHandler();
	var textfields=document.getElementsByName("q");
	for(var i=0;i<textfields.length;i++) {
		var textfield = textfields[i];
		textfield.addEventListener("keydown" , function(e){
			var ch=keybind.keycode2char(e);
			log(ch);
			if(ch == "C-["){ // alternative ESC
				ev.enable();
			}
		}, false);
		textfield.addEventListener("focus", function(){
			log('focus');
			ev.disable();
		}, false);
		textfield.addEventListener("blur", function(){
			log('blur');
			ev.enable();
		}, false);
	}


	//**************************************************
	// setup
	//**************************************************
	ev.enable();
	panel.pin.update();
	panel.appendChild(panel.pin.element);
	panel.appendChild(panel.list.element);
	document.body.appendChild(panel);
	panel.addEventListener("mouseover", function(){
		log('hoge' + this);
		panel.list.display();
	}, false);
	panel.addEventListener("mouseout", function(){
		panel.list.hidden();
	}, false);

	keybinds={
		"p": function(){
			var item=items.getItem();
			panel.pin.toggle(item.url,item.title,item.element);
			log(item);
		}
		,"S-p": function(){
			var item=items.getItem();
			panel.pin.toggle(item.cache,"(cache)"+item.title,item.cacheElement);
		}
		,"l": function(){
			panel.list.toggle();
		}
		,"o": function(){
			panel.pin.openAll();
		}
		,"C-S-c": function(){
			panel.pin.clear();
		}
		,"c": function(){ // toggle description text
			css.toggle("div.g table","display:none;","ldr_text");
		}
		,"f": function(){ // focus textfield
			document.getElementsByName("q")[0].focus();
		}
		// ">" input
		,"S->": function(){ // next page
			var d=document.getElementById("nn");
			if(d){
				location.href=d.parentNode.href;
			}
		}
		// "<" input
		,"S-<": function(){ // prev page
			var d=document.getElementById("np");
			if(d){
				location.href=d.parentNode.href;
			}
		}
		,"j": function(){
			items.next();
		}
		,"S-j": function(){
			items.bottom();
		}
		,"k": function(){
			items.prev();
		}
		,"S-k": function(){
			items.top();
		}
	}
})();

/**
作業ログ。
・onkeypress = function() { ... } などの行でエラーが発生している。
  → 全て addEventListener を使うように書き換え。
     → エラーは出なくなった。が、なにも機能はしていないようだ。
・logを挿入してイベントが正しく補足できているかチェック。
  → keypressに対するイベントハンドラの登録先を document
     から document.getElementsByName("html")[0] に変更。
  → イベントは受けれるようになったが、event.keyCodeが取得できていない(いつも0)。
     対象イベントを "keypress" から "keydown" に変更。
  → []<> のキーで keycode2char() の戻り値がnullになっている。
     ハッシュのキーコードが間違っているので正しい値(188, 190, 219, 221)を追加。
・jを押すと、何か動くみたいだけど、ページが再読込されてしまう。
  → ソースを見てみると、location.hashを書き換えてスクロールさせているようだ。
  → しかも、書き換えた後で location.hash="" として、変更をクリアしている。
     → これ要らないじゃね? コメントアウトする。
        → j, k で動くようになった。
・pを押してピン状態にしても画面に変化が無い。(oを押すと開くのでピンは刺さっているらしい)
  → ソースを見てみるとclassNameの変更をやっているようだが反応が無いらしい。
  → ためしに element.style.border = "3px dotted orange"; として見る。
     → ピンを着けた項目にボーダーが正常に表示される。
     → css.addRuleが効いてないじゃないか?ソースを良く見てみる。
        → 怪しい箇所発見。 elm.innerText=selector+"{"+property+"}";
        → innerTextプロパティはIEの独自拡張であり、IEとIEに対して最大限の互換性を
           得るために頑張っているOperaでしか使えないので、innerHTMLにして見る。
           → スタイルが変わるようになった。
           → innerHTMLなので、ほんとはエスケープが要るんだけど、まあいいか。
     → もう要らないので、element.style.borderの書き換えを消す。
・panel.pin.toggle=function(){ ... } は argument と apply関数を駆使していたが、
  まったくその必要性が感じられないので、修正。
・あと、何の機能があるの?
  → S-kでページ末尾、S-jでページ頭らしい。
     → S-kは動くけど、S-jは先頭の次にジャンプしてる。
        → this.current=1; → 怪しいので、0にする。
           → 先頭にジャンプした。
・ずっと気になってた"char"(JavaScriptでは予約語。つかわれていなけど)を"ch"に置換する。
・こんなもんかな?
  → 問題発覚。テキストエリア(下の方)に j とか k とか打てない orz
  → document.getElementsByName("q")に対してループさせる。
・そういえば、現在指しているpinの数が表示されるんじゃなかったっけ?
  → pin.list.update にも innerText を使っている箇所があったので修正 

・せっかくだから Trixie でも動くようにしようかと思ったけど、 position:fixed 
  なんてリッチな機能を使ってるのでやめた。
*/

上記の通りの修正で無事FirefoxGreasemonkeyで動かすことができた。
ただし、別の問題も発生した。
逆にOperaで動かなくっていることだ。*1
クロスブラウザの道のりは険しい…。

*1:あとで直すつもり→ 原因は、修正中のゴミがのこっていたせいでした。 Opera9.0.2で上記修正済みソースで動きました。