diff --git a/common/content/browser.js b/common/content/browser.js index 826c2a6b..1e4d2dc8 100644 --- a/common/content/browser.js +++ b/common/content/browser.js @@ -133,7 +133,7 @@ var Browser = Module("browser", { }, commands: function () { - commands.add(["downl[oads]", "dl"], + commands.add(["old-downl[oads]", "old-dl"], "Show progress of current downloads", function () { dactyl.open("chrome://mozapps/content/downloads/downloads.xul", diff --git a/common/content/buffer.js b/common/content/buffer.js index fe3adeaf..fea890f6 100644 --- a/common/content/buffer.js +++ b/common/content/buffer.js @@ -786,6 +786,10 @@ var Buffer = Module("buffer", { var persist = services.Persist(); persist.persistFlags = persist.PERSIST_FLAGS_FROM_CACHE | persist.PERSIST_FLAGS_REPLACE_EXISTING_FILES; + + persist.progressListener = new window.DownloadListener(window, + services.Transfer(uri, services.io.newFileURI(file), "", + null, null, null, persist)); persist.saveURI(uri, null, null, null, null, file); }, { autocomplete: true, diff --git a/common/content/commandline.js b/common/content/commandline.js index aa1b5c8c..6ef6f71c 100644 --- a/common/content/commandline.js +++ b/common/content/commandline.js @@ -265,10 +265,12 @@ var CommandWidgets = Class("CommandWidgets", { while (elem.contentDocument.documentURI != elem.getAttribute("src") || ["viewable", "complete"].indexOf(elem.contentDocument.readyState) < 0) util.threadYield(); - return elem; + res = res || (processor || util.identity).call(self, elem); + return res; } }); - return Class.replaceProperty(this, name, (processor || util.identity).call(this, this[name])) + let res, self = this; + return Class.replaceProperty(this, name, this[name]) }, get completionList() this._whenReady("completionList", "dactyl-completions"), @@ -748,8 +750,15 @@ var CommandLine = Module("commandline", { XML.ignoreWhitespace = false; XML.prettyPrinting = false; let style = typeof str === "string" ? "pre" : "nowrap"; - this._lastMowOutput =
{str}
; - let output = util.xmlToDom(this._lastMowOutput, doc); + if (callable(str)) { + this._lastMowOutput = null; + var output = util.xmlToDom(
, doc); + output.appendChild(str(doc)); + } + else { + this._lastMowOutput =
{str}
; + var output = util.xmlToDom(this._lastMowOutput, doc); + } // FIXME: need to make sure an open MOW is closed when commands // that don't generate output are executed @@ -824,7 +833,7 @@ var CommandLine = Module("commandline", { let single = flags & (this.FORCE_SINGLELINE | this.DISALLOW_MULTILINE); let action = this._echoLine; - if ((flags & this.FORCE_MULTILINE) || (/\n/.test(str) || typeof str == "xml") && !(flags & this.FORCE_SINGLELINE)) + if ((flags & this.FORCE_MULTILINE) || (/\n/.test(str) || !isString(str)) && !(flags & this.FORCE_SINGLELINE)) action = this._echoMultiline; if (single) @@ -1073,63 +1082,69 @@ var CommandLine = Module("commandline", { // FIXME: if 'more' is set and the MOW is not scrollable we should still // allow a down motion after an up rather than closing onMultilineOutputEvent: function onMultilineOutputEvent(event) { - const KILL = false, PASS = true; + try { + const KILL = false, PASS = true; - let win = this.widgets.multilineOutput.contentWindow; - let elem = win.document.documentElement; + let win = this.widgets.multilineOutput.contentWindow; + let elem = win.document.documentElement; - let key = events.toString(event); + let key = events.toString(event); - function openLink(where) { - event.preventDefault(); - dactyl.open(event.target.href, where); - } - - // TODO: Wouldn't multiple handlers be cleaner? --djk - if (event.type == "click" && event.target instanceof HTMLAnchorElement) { - - let command = event.originalTarget.getAttributeNS(NS.uri, "command"); - if (command && dactyl.commands[command]) { + const openLink = function openLink(where) { event.preventDefault(); - return dactyl.withSavedValues(["forceNewTab"], function () { - dactyl.forceNewTab = event.ctrlKey || event.shiftKey || event.button == 1; - return dactyl.commands[command](event); - }); + dactyl.open(event.target.href, where); } - switch (key) { - case "": - event.preventDefault(); - openLink(dactyl.CURRENT_TAB); - return KILL; - case "": - case "": - case "": - openLink({ where: dactyl.NEW_TAB, background: true }); - return KILL; - case "": - case "": - case "": - openLink({ where: dactyl.NEW_TAB, background: false }); - return KILL; - case "": - openLink(dactyl.NEW_WINDOW); - return KILL; + // TODO: Wouldn't multiple handlers be cleaner? --djk + if (event.type == "click" && (event.target instanceof HTMLAnchorElement || + event.originalTarget.hasAttributeNS(NS, "command"))) { + + let command = event.originalTarget.getAttributeNS(NS, "command"); + if (command && dactyl.commands[command]) { + event.preventDefault(); + return dactyl.withSavedValues(["forceNewTab"], function () { + dactyl.forceNewTab = event.ctrlKey || event.shiftKey || event.button == 1; + return dactyl.commands[command](event); + }); + } + + switch (key) { + case "": + event.preventDefault(); + openLink(dactyl.CURRENT_TAB); + return KILL; + case "": + case "": + case "": + openLink({ where: dactyl.NEW_TAB, background: true }); + return KILL; + case "": + case "": + case "": + openLink({ where: dactyl.NEW_TAB, background: false }); + return KILL; + case "": + openLink(dactyl.NEW_WINDOW); + return KILL; + } + return PASS; } - return PASS; + + if (event instanceof MouseEvent) + return KILL; + + const atEnd = function atEnd(dir) !Buffer.isScrollable(elem, dir || 1); + + if (!options["more"] || atEnd(1)) { + modes.pop(); + events.feedkeys(key); + } + else + commandline.updateMorePrompt(false, true); } - - if (event instanceof MouseEvent) - return KILL; - - function atEnd(dir) !Buffer.isScrollable(elem, dir || 1); - - if (!options["more"] || atEnd(1)) { - modes.pop(); - events.feedkeys(key); + catch (e) { + util.reportError(e); } - else - commandline.updateMorePrompt(false, true); return PASS; }, diff --git a/common/content/dactyl.js b/common/content/dactyl.js index 1acf1810..da8fbfb8 100644 --- a/common/content/dactyl.js +++ b/common/content/dactyl.js @@ -53,16 +53,18 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), { }, observers: { - "dactyl-cleanup": function () { + "dactyl-cleanup": function dactyl_cleanup() { let modules = dactyl.modules; for (let name in values(Object.getOwnPropertyNames(modules).reverse())) { let mod = Object.getOwnPropertyDescriptor(modules, name).value; if (mod instanceof Class) { if ("cleanup" in mod) - mod.cleanup(); + this.trapErrors(mod.cleanup, mod); if ("destroy" in mod) - mod.destroy(); + this.trapErrors(mod.destroy, mod); + if ("INIT" in mod && "cleanup" in mod.INIT) + this.trapErrors(mod.cleanup, mod, dactyl, modules, window); } } @@ -360,6 +362,9 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), { }, userEval: function (str, context, fileName, lineNumber) { + if (jsmodules.__proto__ != window) + str = "with (window) { with (modules) { this.eval(" + str.quote() + ") } }"; + if (fileName == null) if (io.sourcing && io.sourcing.file[0] !== "[") ({ file: fileName, line: lineNumber }) = io.sourcing; @@ -389,9 +394,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), { if (!context) context = _userContext; - if (window.isPrototypeOf(modules)) - return Cu.evalInSandbox(str, context, "1.8", fileName, lineNumber); - return Cu.evalInSandbox("with (window) { with (modules) { this.eval(" + str.quote() + ") } }", context, "1.8", fileName, lineNumber); + return Cu.evalInSandbox(str, context, "1.8", fileName, lineNumber); }, /** @@ -2178,7 +2181,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), { let init = services.environment.get(config.idName + "_INIT"); let rcFile = io.getRCFile("~"); - if (dactyl.userEval('typeof document') === "undefined") + if (dactyl.userEval("typeof document", null, "test.js") === "undefined") jsmodules.__proto__ = XPCSafeJSObjectWrapper(window); try { diff --git a/common/content/modes.js b/common/content/modes.js index c7b31a82..8a1694cd 100644 --- a/common/content/modes.js +++ b/common/content/modes.js @@ -175,6 +175,9 @@ var Modes = Module("modes", { } }); }, + cleanup: function () { + modes.reset(); + }, _getModeMessage: function () { // when recording a macro diff --git a/common/locale/en-US/gui.xml b/common/locale/en-US/gui.xml index d38183d2..02afea01 100644 --- a/common/locale/en-US/gui.xml +++ b/common/locale/en-US/gui.xml @@ -70,9 +70,8 @@ :downloads

- Show progress of current downloads. Open the standard &dactyl.host; - download dialog in a new tab. Here, downloads can be paused, - resumed, and canceled. + Show progress of current downloads. Here, downloads can + be paused, resumed, and canceled.

diff --git a/common/modules/base.jsm b/common/modules/base.jsm index 68e875f0..bbc3e47a 100644 --- a/common/modules/base.jsm +++ b/common/modules/base.jsm @@ -737,9 +737,9 @@ Class.memoize = function memoize(getter) configurable: true, enumerable: true, init: function (key) { - this.get = function replace() ( - Class.replaceProperty(this, key, null), - Class.replaceProperty(this, key, getter.call(this, key))) + this.get = function replace() let (obj = this.instance || this) ( + Class.replaceProperty(obj, key, null), + Class.replaceProperty(obj, key, getter.call(this, key))) } }); @@ -1196,6 +1196,9 @@ update(iter, { return undefined; }, + sort: function sort(iter, fn, self) + array(this.toArray(iter).sort(fn, self)), + uniq: function uniq(iter) { let seen = {}; for (let item in iter) diff --git a/common/modules/config.jsm b/common/modules/config.jsm index 17378772..0c86d3d9 100644 --- a/common/modules/config.jsm +++ b/common/modules/config.jsm @@ -426,9 +426,16 @@ var ConfigBase = Class("ConfigBase", { Keyword color: red; Tag color: blue; - Usage position: relative; padding-right: 2em; - Usage>LineInfo position: absolute; left: 100%; padding: 1ex; margin: -1ex -1em; background: rgba(255, 255, 255, .8); border-radius: 1ex; - Usage:not(:hover)>LineInfo opacity: 0; left: 0; width: 1px; height: 1px; overflow: hidden; + Link position: relative; padding-right: 2em; + Link:not(:hover)>LinkInfo opacity: 0; left: 0; width: 1px; height: 1px; overflow: hidden; + LinkInfo { + position: absolute; + left: 100%; + padding: 1ex; + margin: -1ex -1em; + background: rgba(255, 255, 255, .8); + border-radius: 1ex; + } StatusLine;;;FontFixed { -moz-appearance: none !important; @@ -490,6 +497,30 @@ var ConfigBase = Class("ConfigBase", { HintActive;;* background-color: #88FF00 !important; color: black !important; HintImage;;* opacity: .5 !important; + Button display: inline-block; font-weight: bold; cursor: pointer; + Button:hover text-decoration: underline; + Button[collapsed] visibility: collapse; width: 0; + Button::before content: "["; color: gray; text-decoration: none !important; + Button::after content: "]"; color: gray; text-decoration: none !important; + Button:not([collapsed]) ~ Button:not([collapsed])::before content: "/["; + + Downloads display: table; margin: 0; padding: 0; + DownloadHead;;;CompTitle display: table-row; + DownloadHead>*;;;DownloadCell display: table-cell; + + Download display: table-row; + + DownloadCell display: table-cell; padding: 0 1ex; + DownloadButtons;;;DownloadCell + DownloadPercent;;;DownloadCell + DownloadProgress;;;DownloadCell + DownloadProgressHave + DownloadProgressTotal + DownloadSource;;;DownloadCell,URL + DownloadState;;;DownloadCell + DownloadTime;;;DownloadCell + DownloadTitle;;;DownloadCell,URL + // ]]>, / /g, "\n")), diff --git a/common/modules/downloads.jsm b/common/modules/downloads.jsm new file mode 100644 index 00000000..11592d0e --- /dev/null +++ b/common/modules/downloads.jsm @@ -0,0 +1,268 @@ +// Copyright (c) 2011 by Kris Maglione +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +Components.utils.import("resource://dactyl/bootstrap.jsm"); +defineModule("downloads", { + exports: ["Download", "Downloads", "downloads"], + use: ["io", "services", "template", "util"] +}, this); + +Cu.import("resource://gre/modules/DownloadUtils.jsm", this); + +let prefix = "DOWNLOAD_"; +var states = iter([v, k.slice(prefix.length).toLowerCase()] + for ([k, v] in Iterator(Ci.nsIDownloadManager)) + if (k.indexOf(prefix) == 0)) + .toObject(); + +var Download = Class("Download", { + init: function init(id, document) { + let self = XPCSafeJSObjectWrapper(services.downloadManager.getDownload(id)); + self.__proto__ = this; + this.instance = this; + + this.nodes = {}; + util.xmlToDom( +
  • + + + {self.displayName} + {self.targetFile.path} + + + + + Pause + Remove + Resume + Retry + Cancel + Delete + + + / + + + + {self.source.spec} +
  • , + document, this.nodes); + + for (let [key, node] in Iterator(this.nodes)) { + node.dactylDownload = self; + if (node.getAttributeNS(NS, "highlight") == "Button") { + node.setAttributeNS(NS, "command", "download.command"); + update(node, { + set collapsed(collapsed) { + if (collapsed) + this.setAttribute("collapsed", "true"); + else + this.removeAttribute("collapsed"); + }, + get collapsed() !!this.getAttribute("collapsed") + }); + } + } + + self.updateStatus(); + return self; + }, + + get status() states[this.state], + + inState: function inState(states) states.indexOf(this.status) >= 0, + + get alive() this.inState(["downloading", "notstarted", "paused", "queued", "scanning"]), + + allowed: Class.memoize(function () let (self = this) ({ + get cancel() self.cancelable && self.inState(["downloading", "paused", "starting"]), + get delete() !this.cancel && self.targetFile.exists(), + get pause() self.inState(["downloading"]), + get remove() self.inState(["blocked_parental", "blocked_policy", + "canceled", "dirty", "failed", "finished"]), + get resume() self.resumable && self.inState(["paused"]), + get retry() self.inState(["canceled", "failed"]) + })), + + command: function command(name) { + util.assert(set.has(this.allowed, name), "Unknown command"); + util.assert(this.allowed[name], "Command not allowed"); + + if (set.has(this.commands, name)) + this.commands[name].call(this); + else + services.downloadManager[name + "Download"](this.id); + }, + + commands: { + delete: function delete() { + this.targetFile.remove(false); + this.updateStatus(); + } + }, + + compare: function compare(other) String.localeCompare(this.displayName, other.displayName), + + timeRemaining: Infinity, + + updateProgress: function updateProgress() { + let self = this.__proto__; + + if (this.amountTransferred === this.size) + this.nodes.time.textContent = ""; + else if (this.speed == 0 || this.size == 0) + this.nodes.time.textContent = "Unknown"; + else { + let seconds = (this.size - this.amountTransferred) / this.speed; + [, self.timeRemaining] = DownloadUtils.getTimeLeft(seconds, this.timeRemaining); + if (this.timeRemaining) + this.nodes.time.textContent = util.formatSeconds(this.timeRemaining); + else + this.nodes.time.textContent = "~1 second"; + } + let total = this.nodes.progressTotal.textContent = this.size ? util.formatBytes(this.size, 1, true) : "Unknown"; + let suffix = RegExp(/( [a-z]+)?$/i.exec(total)[0] + "$"); + this.nodes.progressHave.textContent = util.formatBytes(this.amountTransferred, 1, true).replace(suffix, ""); + + this.nodes.percent.textContent = this.size ? Math.round(this.amountTransferred * 100 / this.size) + "%" : ""; + }, + + updateStatus: function updateStatus() { + + this.nodes.state.textContent = util.capitalize(this.status); + for (let [command, enabled] in Iterator(this.allowed)) + this.nodes[command].collapsed = !enabled; + this.updateProgress(); + } +}); + +var DownloadList = Class("DownloadList", + XPCOM([Ci.nsIDownloadProgressListener, + Ci.nsIObserver, + Ci.nsISupportsWeakReference]), { + init: function init(document, modules) { + this.modules = modules; + this.document = document; + this.nodes = {}; + util.xmlToDom(
      +
    • + Title + Status + + Progress + + Time remaining + Source +
    • +
    , this.document, this.nodes); + + this.downloads = {}; + for (let row in iter(services.downloadManager.DBConnection + .createStatement("SELECT id FROM moz_downloads"))) + this.addDownload(row.id); + + util.addObserver(this); + services.downloadManager.addListener(this); + }, + cleanup: function cleanup() { + this.observe.unregister(); + services.downloadManager.removeListener(this); + }, + + addDownload: function addDownload(id) { + if (!(id in this.downloads)) { + this.downloads[id] = Download(id, this.document); + + let index = values(this.downloads).sort(function (a, b) a.compare(b)) + .indexOf(this.downloads[id]); + + this.nodes.list.insertBefore(this.downloads[id].nodes.row, + this.nodes.list.childNodes[index + 1]); + } + }, + removeDownload: function removeDownload(id) { + if (id in this.downloads) { + this.nodes.list.removeChild(this.downloads[id].nodes.row); + delete this.downloads[id]; + } + }, + + leave: function leave(stack) { + if (stack.pop) + this.cleanup(); + }, + + observers: { + "download-manager-remove-download": function (id) { + if (id == null) + id = [k for ([k, dl] in iter(this.downloads)) if (dl.allowed.remove)]; + else + id = [id.QueryInterface(Ci.nsISupportsPRUint32).data]; + + Array.concat(id).map(this.closure.removeDownload); + } + }, + + onDownloadStateChange: function (state, download) { + try { + if (download.id in this.downloads) + this.downloads[download.id].updateStatus(); + else { + this.addDownload(download.id); + + this.modules.commandline.updateOutputHeight(true); + this.nodes.list.scrollIntoView(false); + } + } + catch (e) { + util.reportError(e); + } + }, + onProgressChange: function (webProgress, request, + curProgress, maxProgress, + curTotalProgress, maxTotalProgress, + download) { + try { + if (download.id in this.downloads) + this.downloads[download.id].updateProgress(); + } + catch (e) { + util.reportError(e); + } + } +}); + +var Downloads = Module("downloads", { +}, { +}, { + commands: function (dactyl, modules, window) { + const { commands } = modules; + + commands.add(["downl[oads]", "dl"], + "Display the downloads list", + function (args) { + modules.commandline.echo(function (doc) { + let downloads = DownloadList(doc, modules); + // Temporary and dangerous hack: + modules.modes.getStack(0).params = downloads; + return downloads.nodes.list; + }); + }); + }, + dactyl: function (dactyl, modules, window) { + dactyl.commands["download.command"] = function (event) { + let elem = event.originalTarget; + elem.dactylDownload.command(elem.getAttribute("key")); + } + } +}); + +endModule(); + +// catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); } + +// vim: set fdm=marker sw=4 ts=4 et ft=javascript: diff --git a/common/modules/highlight.jsm b/common/modules/highlight.jsm index 08fc7633..e982d09e 100644 --- a/common/modules/highlight.jsm +++ b/common/modules/highlight.jsm @@ -92,7 +92,8 @@ var Highlights = Module("Highlight", { keys: function keys() Object.keys(this.highlight).sort(), - __iterator__: function () values(this.highlight), + __iterator__: function () values(this.highlight).sort(function (a, b) String.localeCompare(a.class, b.class)) + .iterValues(), _create: function (agent, args) { let obj = Highlight.apply(Highlight, args); @@ -318,7 +319,7 @@ var Highlights = Module("Highlight", { if (!modify) modules.commandline.commandOutput( template.tabular(["Key", "Sample", "Link", "CSS"], - ["padding: 0 1em 0 0; vertical-align: top", + ["padding: 0 1em 0 0; vertical-align: top; max-width: 16em; overflow: hidden;", "text-align: center"], ([h.class, XXX, diff --git a/common/modules/overlay.jsm b/common/modules/overlay.jsm index 6ac9972e..7da5e57f 100644 --- a/common/modules/overlay.jsm +++ b/common/modules/overlay.jsm @@ -149,6 +149,7 @@ var Overlay = Module("Overlay", { ["base", "completion", "config", + "downloads", "javascript", "overlay", "prefs", diff --git a/common/modules/services.jsm b/common/modules/services.jsm index 5e461ed5..5d1bc915 100644 --- a/common/modules/services.jsm +++ b/common/modules/services.jsm @@ -79,6 +79,7 @@ var Services = Module("Services", { [Ci.nsIChannel, Ci.nsIInputStreamChannel, Ci.nsIRequest], "setURI"); this.addClass("String", "@mozilla.org/supports-string;1", Ci.nsISupportsString, "data"); this.addClass("StringStream", "@mozilla.org/io/string-input-stream;1", Ci.nsIStringInputStream, "data"); + this.addClass("Transfer", "@mozilla.org/transfer;1", Ci.nsITransfer, "init"); this.addClass("Timer", "@mozilla.org/timer;1", Ci.nsITimer, "initWithCallback"); this.addClass("StreamCopier", "@mozilla.org/network/async-stream-copier;1",Ci.nsIAsyncStreamCopier, "init"); this.addClass("Xmlhttp", "@mozilla.org/xmlextras/xmlhttprequest;1", Ci.nsIXMLHttpRequest); diff --git a/common/modules/storage.jsm b/common/modules/storage.jsm index 3c25b31d..03ecd413 100644 --- a/common/modules/storage.jsm +++ b/common/modules/storage.jsm @@ -259,9 +259,15 @@ var Storage = Module("Storage", { } }, { }, { - init: function (dactyl, modules) { + init: function init(dactyl, modules) { + init.superapply(this, arguments); storage.infoPath = File(modules.IO.runtimePath.replace(/,.*/, "")) .child("info").child(dactyl.profileName); + }, + + cleanup: function (dactyl, modules, window) { + delete window.dactylStorageRefs; + this.removeDeadObservers(); } }); diff --git a/common/modules/template.jsm b/common/modules/template.jsm index 20ff431d..53aaf216 100644 --- a/common/modules/template.jsm +++ b/common/modules/template.jsm @@ -331,11 +331,11 @@ var Template = Module("Template", { this.map(iter, function (item) - { + { let (name = item.name || item.names[0], frame = item.definedAt) !frame ? name : template.helpLink(help(item), name, "Title") + - Defined at {sourceLink(frame)} + Defined at {sourceLink(frame)} } {desc(item)} diff --git a/common/modules/util.jsm b/common/modules/util.jsm index 92ac06c2..bdea56e6 100644 --- a/common/modules/util.jsm +++ b/common/modules/util.jsm @@ -135,6 +135,13 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), throw FailedAssertion(message, 1); }, + /** + * Capitalizes the first character of the given string. + * @param {string} str The string to capitalize + * @returns {string} + */ + capitalize: function capitalize(str) str && str[0].toUpperCase() + str.slice(1), + /** * Returns a RegExp object that matches characters specified in the range * expression *list*, or signals an appropriate error if *list* is invalid. @@ -499,9 +506,10 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), stackLines: function (stack) { let lines = []; - let match, re = /([^]*?)(@[^@\n]*)(?:\n|$)/g; + let match, re = /([^]*?)@([^@\n]*)(?:\n|$)/g; while (match = re.exec(stack)) - lines.push(match[1].replace(/\n/g, "\\n").substr(0, 80) + match[2]); + lines.push(match[1].replace(/\n/g, "\\n").substr(0, 80) + "@" + + match[2].replace(/.* -> /, "")); return lines; }, @@ -653,6 +661,28 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), return strNum[0] + " " + unitVal[unitIndex]; }, + /** + * Converts *seconds* into a human readable time string. + * + * @param {number} seconds + * @returns {string} + */ + formatSeconds: function formatSeconds(seconds) { + function div(num, denom) [Math.round(num / denom), Math.round(num % denom)]; + let days, hours, minutes; + + [minutes, seconds] = div(seconds, 60); + [hours, minutes] = div(minutes, 60); + [days, hours] = div(hours, 24); + if (days) + return days + " days " + hours + " hours" + if (hours) + return hours + "h " + minutes + "m"; + if (minutes) + return minutes + ":" + seconds; + return seconds + "s"; + }, + /** * Returns the file which backs a given URL, if available. * diff --git a/pentadactyl/NEWS b/pentadactyl/NEWS index 4fecca2d..94862607 100644 --- a/pentadactyl/NEWS +++ b/pentadactyl/NEWS @@ -64,6 +64,8 @@ consistent interactive help facility (improvements include listing keys for modes other than Normal, filtering the output and linking to source locations). + - :downloads now opens a download list in the multi-line output + buffer. - Added :cookies command. * :extadd now supports remote URLs as well as local files on Firefox 4. * Added :if/:elseif/:else/:endif conditionals.