Hibernate Idle Tabs

If a tab is unused for a long time, it switches to a light holding page until the tab is focused again. This helps the browser to recover memory, and can speed up the re-opening of large sessions.

2015-01-24 या दिनांकाला. सर्वात नवीन आवृत्ती पाहा.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name           Hibernate Idle Tabs
// @namespace      HIT
// @description    If a tab is unused for a long time, it switches to a light holding page until the tab is focused again.  This helps the browser to recover memory, and can speed up the re-opening of large sessions.
// @version        1.1.2
// @downstreamURL  http://userscripts.org/scripts/source/123252.user.js
// @include        *
// ==/UserScript==


/* +++ Config +++ */

var hibernateIfIdleForMoreThan = 4*60*60; // 4 hours
var restoreTime = 0.5; // in seconds

// We need an always-available basically blank HTML page we can navigate to for
// hibernation.  The userscript will run there and await re-activation.
// Userscripts do not run on about:blank in Firefox 6.0 or Chromium 2011, but a
// file:///... URL might work.

// I got desperate and aimed for a 404 on Google:
// This is not really satisfactory, since it provides an image and unneeded CSS!
var holdingPage = "http://neuralyte.org/~joey/hibernated_tab.html";

// If you want to use another holding page, put the old one here to keep your
// current/saved browser sessions working.
var oldHoldingPage = "http://www.google.com/hibernated_tab";

var passFaviconToHoldingPage = true;
var fadeHibernatedFavicons = true;

var forceHibernateWhenRunTwice = true;



/* +++ Documentation +++ */
//
// On a normal page, checks to see if the user goes idle.  (Mouse movements,
// key actions and page focus reset the idle timer.)  If the page is left idle
// past the timeout, then the window navigates to a lighter holding page,
// hopefully freeing up memory in the browser.
//
// On a holding page, if the user focuses the window, the window navigates back
// to the original page.  (This can be cancelled in Chrome by clicking on
// another tab, but not by paging to another tab with the keyboard!)
//
// I think single-click is also a cancellation now.
//
// In order for the tab of the holding page to present the same favicon as the
// original page, we must capture this image before leaving the original page,
// and pass it to the holding page as a CGI parameter.
//
// (A simpler alternative might be to aim for a 404 on the same domain and use
// that as the holding page.)



// BUG: Sometimes when un-hibernating, the webserver of the page we return to
// complains that the referrer URL header is too long!

// TODO: Some users may want the hibernated page to restore immediately when the tab is *refocused*, rather than waiting for a mouseover.

// TESTING: Expose a function to allow a bookmarklet to force-hibernate the current tab?

// CONSIDER: If we forget about fading the favicon, couldn't we simplify things by just sending the favicon URL rather than its image data?  I think I tested this, and although I could load the favicon into the document, I was not successful at getting it into the browser's title tab by adding a new <link rel="icon">.



/* +++ Main +++ */

var onHoldingPage = document.location.href.match(holdingPage+"?") != null;

// If you change holding page, this keeps the old one working for a while, for
// the sake of running browsers or saved sessions.
if (!onHoldingPage && oldHoldingPage) {
	onHoldingPage = document.location.href.match(oldHoldingPage+"?") != null;
}

function handleNormalPage() {

	whenIdleDo(hibernateIfIdleForMoreThan,hibernatePage);

	function hibernatePage() {

		var params = {
			title: document.title,
			url: document.location.href
		};

		function processFavicon(canvas) {
			document.body.appendChild(canvas);
			if (canvas) {
				try {
					if (fadeHibernatedFavicons) {
						makeCanvasMoreTransparent(canvas);
					}
					var faviconDataURL = canvas.toDataURL("image/png");
					params.favicon_data = faviconDataURL;
				} catch (e) {
					var extra = ( window != top ? " (running in frame or iframe)" : "" );
					console.error("[HIT] Got error"+extra+": "+e+" doc.loc="+document.location.href);
					// We get "Error: SECURITY_ERR: DOM Exception 18" (Chrome) if
					// the favicon is from a different host.
				}
			}
			reallyHibernatePage();
		}

		function reallyHibernatePage() {
			var queryString = buildQueryParameterString(params);
			document.location = holdingPage + "?" + queryString;
		}

		if (passFaviconToHoldingPage) {
			// I don't know how to grab the contents of the current favicon, so we
			// try to directly load a copy for ourselves.
			var url = document.location.href;
			var targetHost = url.replace(/.*:\/\//,'').replace(/\/.*/,'');
			loadFaviconIntoCanvas(targetHost,processFavicon);
		} else {
			reallyHibernatePage();
		}

	}

	function makeCanvasMoreTransparent(canvas) {
		var wid = canvas.width;
		var hei = canvas.height;
		var ctx = canvas.getContext("2d");
		var img = ctx.getImageData(0,0,wid,hei);
		var data = img.data;
		var len = 4*wid*hei;
		for (var ptr=0;ptr<len;ptr+=4) {
			data[ptr+3] /= 4;  // alpha channel
		}
		// May or may not be needed:
		ctx.putImageData(img,0,0);
	}



	if (forceHibernateWhenRunTwice) {
		if (window.hibernate_idle_tabs_loaded) {
			hibernatePage();
		}
		window.hibernate_idle_tabs_loaded = true;
	}


}

function handleHoldingPage() {

	var params = getQueryParameters();

	// setHibernateStatus("Holding page for " + params.title + "\n with URL: "+params.url);
	// var titleReport = params.title + " (Holding Page)";
	var titleReport = "(" + (params.title || params.url) + " :: Hibernated)";
	setWindowTitle(titleReport);

	var mainReport = titleReport;
	if (params.title) {
		/*
		statusElement.appendChild(document.createElement("P"));
		var div = document.createElement("tt");
		div.style.fontSize = "0.8em";
		div.appendChild(document.createTextNode(params.url));
		statusElement.appendChild(div);
		*/
		mainReport += "\n" + params.url;
	}

	setHibernateStatus(mainReport);

	try {
		var faviconDataURL = params.favicon_data;
		if (!faviconDataURL) {
			// If we do not have a favicon, it is preferable to present an empty/transparent favicon, rather than let the browser show the favicon of the holding page site!
			faviconDataURL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAEklEQVQ4jWNgGAWjYBSMAggAAAQQAAF/TXiOAAAAAElFTkSuQmCC";
		}
		writeFaviconFromDataString(faviconDataURL);
	} catch (e) {
		console.error(""+e);
	}

	function restoreTab(evt) {
		var url = decodeURIComponent(params.url);
		setHibernateStatus("Returning to: "+url);
		document.location.replace(url);
		/*
		// Alternative; preserves "forward"
		window.history.back();  // TESTING!  With the fallback below, this seemed to work 90% of the time?
		// Sometimes it doesn't work.  So we fallback to old method:
		setTimeout(function(){
			setHibernateStatus("window.history.back() FAILED - setting document.location");
			setTimeout(function(){
				document.location.replace(url);   // I once saw this put ':'s when it should have put '%35's or whatever.  (That broke 'Up' bookmarklet.)
			},1000);
		},2500);
		*/
		evt.preventDefault();   // Accept responsibility for the double-click.
		return false;   // Prevent browser from doing anything else with it (e.g. selecting the word under the cursor).
	}

	checkForActivity();

	function checkForActivity() {

		var countdownTimer = null;

		// listen(document.body,'mousemove',startCountdown); // In Firefox this ignore mousemove on empty space (outside the document content), so trying window...
		listen(window,'mousemove',startCountdown); // Likewise for click below!
		// listen(document.body,'blur',clearCountdown); // Does not fire in Chrome?
		listen(window,'blur',clearCountdown); // For Chrome
		//listen(window,'mouseout',clearCountdown); // Firefox appears to be firing this when my mouse is still over the window, preventing navigation!  Let's just rely on 'blur' instead.
		// listen(document.body,'click',clearCountdown);
		listen(window,'click',clearCountdown);
		listen(window,'dblclick',restoreTab);

		function startCountdown(e) {
			if (countdownTimer != null) {
				// There is already a countdown running - do not start.
				return;
			}
			var togo = restoreTime*1000;
			function countDown() {
				setHibernateStatus(mainReport +
						'\n' + "Tab will restore in "+(togo/1000).toFixed(1)+" seconds." +
						'  ' + "Click or defocus to pause." +
						'  ' + "Or double click to restore now!"
						);
				if (togo <= 0) {
					restoreTab();
				} else {
					togo -= 1000;
					if (countdownTimer)
						clearTimeout(countdownTimer);
					countdownTimer = setTimeout(countDown,1000);
				}
			}
			countDown();
		}

		function clearCountdown(ev) {
			if (countdownTimer) {
				clearTimeout(countdownTimer);
			}
			countdownTimer = null;
			var evReport = "";
			if (ev) {
				evReport = " by "+ev.type+" on "+this;
			}
			var report = mainReport + '\n' + "Paused" + evReport + "";
			setHibernateStatus(report);
		}

	}

}

if (onHoldingPage) {
	handleHoldingPage();
} else {
	handleNormalPage();
}



/* +++ Library Functions +++ */

function listen(target,eventType,handler,capture) {
	target.addEventListener(eventType,handler,capture);
}

function ignore(target,eventType,handler,capture) {
	target.removeEventListener(eventType,handler,capture);
}

// Given an object, encode its properties and values into a URI-ready CGI query string.
function buildQueryParameterString(params) {
	return Object.keys(params).map( function(key) { return key+"="+encodeURIComponent(params[key]); } ).join("&");
}

// Returns an object whose keys and values match those of the CGI query string of the current document.
function getQueryParameters() {
	var queryString = document.location.search;
	var params = {};
	queryString.replace(/^\?/,'').split("&").map( function(s) {
		var part = s.split("=");
		var key = part[0];
		var value = decodeURIComponent(part[1]);
		params[key] = value;
	});
	return params;
}

function whenIdleDo(idleTimeoutSecs,activateIdleEvent) {

	var timer = null;
	var pageLastUsed = new Date().getTime();

	function setNotIdle() {
		pageLastUsed = new Date().getTime();
	}

	function checkForIdle() {
		var msSinceLastUsed = new Date().getTime() - pageLastUsed;
		if (msSinceLastUsed > idleTimeoutSecs * 1000) {
			activateIdleEvent();
		}
		setTimeout(checkForIdle,idleTimeoutSecs/5*1000);
	}

	setTimeout(checkForIdle,idleTimeoutSecs*1000);

	listen(document.body,'mousemove',setNotIdle);
	listen(document.body,'focus',setNotIdle);
	listen(document.body,'keydown',setNotIdle);

}



/* +++ Local Convenience Functions +++ */

var statusElement = null;
function checkStatusElement() {
	if (!statusElement) {
		while (document.body.firstChild) {
			document.body.removeChild(document.body.firstChild);
		}
		statusElement = document.createElement("div");
		document.body.insertBefore(statusElement,document.body.firstChild);
		statusElement.style.textAlign = "center";
	}
}

function setWindowTitle(msg) {
	msg = ""+msg;
	document.title = msg;
}

function setHibernateStatus(msg) {
	msg = ""+msg;
	checkStatusElement();
	statusElement.textContent = msg;
	statusElement.innerText   = msg;   // IE
	// Currently '\n' works in Chrome, but not in Firefox.
}






/* +++ Favicon, Canvas and DataURL Magic +++ */

function loadFaviconForHost(targetHost,callback) {

	// Try to load a favicon image for the given host, and pass it to callback.
	// Except: If there is a link with rel="icon" in the page, with host
	// matching the current page location, load that image file instead of
	// guessing the extension!

	var favicon = document.createElement('img');
	favicon.addEventListener('load',function() {
		callback(favicon);
	});

	var targetProtocol = document.location.protocol || "http:";

	// If there is a <link rel="icon" ...> in the current page, then I think that overrides the site-global favicon.
	// NOTE: This is not appropriate if a third party targetHost was requested, only if they really wanted the favicon for the current page!
	var foundLink = null;
	var linkElems = document.getElementsByTagName("link");
	for (var i=0;i<linkElems.length;i++) {
		var link = linkElems[i];
		if (link.rel === "icon" || link.rel === "shortcut icon") {
			// Since we can't read the image data of images from third-party hosts, we skip them and keep searching.
			if (link.host == document.location.host) {
				foundLink = link;
				break;
			}
		}
	}
	if (foundLink) {
		favicon.addEventListener('error',function(){ callback(favicon); });
		favicon.src = foundLink.href;   // Won't favicon.onload cause an additional callback to the one below?
		// NOTE: If we made the callback interface pass favicon as 'this' rather than an argument, then we wouldn't need to wrap it here (the argument may be evt).
		favicon = foundLink;
		callback(favicon);
		return;
	}

	var extsToTry = ["jpg","gif","png","ico"];   // iterated in reverse order
	function tryNextExtension() {
		var ext = extsToTry.pop();
		if (ext == null) {
			console.log("Ran out of extensions to try for "+targetHost+"/favicon.???");
			// We run the callback anyway!
			callback(null);
		} else {
			favicon.src = targetProtocol+"//"+targetHost+"/favicon."+ext;
		}
	}
	favicon.addEventListener('error',tryNextExtension);
	tryNextExtension();
	// When the favicon is working we can remove the canvas, but until then we may as well keep it visible!
}

function writeFaviconFromCanvas(canvas) {
	var faviconDataURL = canvas.toDataURL("image/png");
	// var faviconDataURL = canvas.toDataURL("image/x-icon;base64");
	// console.log("Got data URL: "+faviconDataURL.substring(0,10+"... (length "+faviconDataURL.length+")");
	writeFaviconFromDataString(faviconDataURL);
}

function writeFaviconFromDataString(faviconDataURL) {

	var d = document, h = document.getElementsByTagName('head')[0];

	// Create this favicon
	var ss = d.createElement('link');
	ss.rel = 'shortcut icon';
	ss.type = 'image/x-icon';
	ss.href = faviconDataURL;
	/*
	// Remove any existing favicons
	var links = h.getElementsByTagName('link');
	for (var i=0; i<links.length; i++) {
		if (links[i].href == ss.href) return;
		if (links[i].rel == "shortcut icon" || links[i].rel=="icon")
			h.removeChild(links[i]);
	}
	*/
	// Add this favicon to the head
	h.appendChild(ss);

	// Force browser to acknowledge
	// I saw this trick somewhere.  I don't know what browser requires it.  But I just left it in!
	var shim = document.createElement('iframe');
	shim.width = shim.height = 0;
	document.body.appendChild(shim);
	shim.src = "icon";
	document.body.removeChild(shim);

}

function loadFaviconIntoCanvas(targetHost,callback) {

	// console.log("Getting favicon for: "+targetHost);

	var canvas = document.createElement('canvas');
	var ctx = canvas.getContext('2d');

	loadFaviconForHost(targetHost,gotFavicon);

	function gotFavicon(favicon) {
		if (favicon) {
			// console.log("Got favicon from: "+favicon.src);
			canvas.width = favicon.width;
			canvas.height = favicon.height;
			ctx.drawImage( favicon, 0, 0 );
		}
		callback(canvas);
	}

}

/* This throws a security error from canvas.toDataURL(), I think because we are
trying to read something from a different domain than the script!
In Chrome: "SECURITY_ERR: DOM Exception 18"
*/
// loadFaviconIntoCanvas(document.location.host,writeFaviconFromCanvas);