// ==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.3.0
// @downstreamURL http://userscripts.org/scripts/source/123252.user.js
// @include *
// ==/UserScript==
/* +++ Config +++ */
var hibernateIfIdleForMoreThan = 36*60*60; // 36 hours
var restoreTime = 0.5; // in seconds
// We need an always-available basically blank HTML page we can navigate to
// when we hibernate a tab. The userscript will run on that page, and await
// re-activation.
//
// This page is not really ideal, since it provides an image and unneeded CSS.
//
// Also NOTE FOR SECURITY that whatever page you navigate to, the server admin
// will be able to see which page you hibernated, in their logfile!
//
// If you have a blank page somewhere on the net, belonging to an admin you
// trust, I recommend using that instead.
//
var holdingPage = "https://www.google.com/hibernated_tab";
// Throws error: Not allowed to navigate top frame to data URL
//var holdingPage = "data:text/html,<html><head><title>Hibernate Idle Tabs</title></head><body></body></html>";
// If you do change the holding, put the old one here, so that any existing
// hibernated tabs still on the old page will be able to unhibernate.
//
var oldHoldingPages = ["http://neuralyte.org/~joey/hibernated_tab.html", "http://www.google.com/hibernated_tab", "https://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.)
//
// If you use Google Chrome or Chromium, then I would recommend using this
// extension instead, which provides exactly the same functionality, but with
// better security and probaly better performance too:
//
// https://chrome.google.com/webstore/detail/the-great-suspender/klbibkeccnjlkjkiokjodocebajanakg
// (TODO: This aforementioned security concern could probably be fixed by passing data to the target page using # rather than ? - although it would only prevent the data from being passed over HTTP, but Javascript running on the target page could still read it.)
//
// Sadly, userscripts do not run on about:blank in Firefox 6.0 or Chromium 2011. I doubt a file:///... URL would work either.
// 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.
oldHoldingPages.forEach(oldHoldingPage => {
if (document.location.href.match(oldHoldingPage+"?")) {
onHoldingPage = true;
}
});
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);