Change all occurrences of del.icio.us to delicious.com
[conkeror.git] / modules / download-manager.js
blob7e8dceb981c84728fe33f40ce5163ea99e979b10
1 /**
2  * (C) Copyright 2008 Jeremy Maitin-Shepard
3  * (C) Copyright 2009 John Foerch
4  *
5  * Use, modification, and distribution are subject to the terms specified in the
6  * COPYING file.
7 **/
9 require("special-buffer.js");
10 require("mime-type-override.js");
11 require("minibuffer-read-mime-type.js");
13 var download_manager_service = Cc["@mozilla.org/download-manager;1"]
14     .getService(Ci.nsIDownloadManager);
16 var download_manager_builtin_ui = Components
17     .classesByID["{7dfdf0d1-aff6-4a34-bad1-d0fe74601642}"]
18     .getService(Ci.nsIDownloadManagerUI);
21 var unmanaged_download_info_list = [];
22 var id_to_download_info = {};
24 // Import these constants for convenience
25 const DOWNLOAD_NOTSTARTED = Ci.nsIDownloadManager.DOWNLOAD_NOTSTARTED;
26 const DOWNLOAD_DOWNLOADING = Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING;
27 const DOWNLOAD_FINISHED = Ci.nsIDownloadManager.DOWNLOAD_FINISHED;
28 const DOWNLOAD_FAILED = Ci.nsIDownloadManager.DOWNLOAD_FAILED;
29 const DOWNLOAD_CANCELED = Ci.nsIDownloadManager.DOWNLOAD_CANCELED;
30 const DOWNLOAD_PAUSED = Ci.nsIDownloadManager.DOWNLOAD_PAUSED;
31 const DOWNLOAD_QUEUED = Ci.nsIDownloadManager.DOWNLOAD_QUEUED;
32 const DOWNLOAD_BLOCKED = Ci.nsIDownloadManager.DOWNLOAD_BLOCKED;
33 const DOWNLOAD_SCANNING = Ci.nsIDownloadManager.DOWNLOAD_SCANNING;
36 const DOWNLOAD_NOT_TEMPORARY = 0;
37 const DOWNLOAD_TEMPORARY_FOR_ACTION = 1;
38 const DOWNLOAD_TEMPORARY_FOR_COMMAND = 2;
40 function download_info (source_buffer, mozilla_info, target_file) {
41     this.source_buffer = source_buffer;
42     this.target_file = target_file;
43     if (mozilla_info != null)
44         this.attach(mozilla_info);
46 download_info.prototype = {
47     attach : function (mozilla_info) {
48         if (!this.target_file)
49             this.__defineGetter__("target_file", function(){
50                     return this.mozilla_info.targetFile;
51                 });
52         else if (this.target_file.path != mozilla_info.targetFile.path)
53             throw interactive_error("Download target file unexpected.");
54         this.mozilla_info = mozilla_info;
55         id_to_download_info[mozilla_info.id] = this;
56         download_added_hook.run(this);
57     },
59     target_file : null,
61     shell_command : null,
63     shell_command_cwd : null,
65     temporary_status : DOWNLOAD_NOT_TEMPORARY,
67     action_description : null,
69     set_shell_command : function (str, cwd) {
70         this.shell_command = str;
71         this.shell_command_cwd = cwd;
72         if (this.mozilla_info)
73             download_shell_command_change_hook.run(this);
74     },
76     /**
77      * None of the following members may be used until attach is called
78      */
80     // Reflectors to properties of nsIDownload
81     get state () { return this.mozilla_info.state; },
82     get display_name () { return this.mozilla_info.displayName; },
83     get amount_transferred () { return this.mozilla_info.amountTransferred; },
84     get percent_complete () { return this.mozilla_info.percentComplete; },
85     get size () {
86         var s = this.mozilla_info.size;
87         /* nsIDownload.size is a PRUint64, and will have value
88          * LL_MAXUINT (2^64 - 1) to indicate an unknown size.  Because
89          * JavaScript only has a double numerical type, this value
90          * cannot be represented exactly, so 2^36 is used instead as the cutoff. */
91         if (s < 68719476736 /* 2^36 */)
92             return s;
93         return -1;
94     },
95     get source () { return this.mozilla_info.source; },
96     get start_time () { return this.mozilla_info.startTime; },
97     get speed () { return this.mozilla_info.speed; },
98     get MIME_info () { return this.mozilla_info.MIMEInfo; },
99     get MIME_type () {
100         if (this.MIME_info)
101             return this.MIME_info.MIMEType;
102         return null;
103     },
104     get id () { return this.mozilla_info.id; },
105     get referrer () { return this.mozilla_info.referrer; },
107     target_file_text : function () {
108         let target = this.target_file.path;
109         let display = this.display_name;
110         if (target.indexOf(display, target.length - display.length) == -1)
111             target += " (" + display + ")";
112         return target;
113     },
115     throw_if_removed : function () {
116         if (this.removed)
117             throw interactive_error("Download has already been removed from the download manager.");
118     },
120     throw_state_error : function () {
121         switch (this.state) {
122         case DOWNLOAD_DOWNLOADING:
123             throw interactive_error("Download is already in progress.");
124         case DOWNLOAD_FINISHED:
125             throw interactive_error("Download has already completed.");
126         case DOWNLOAD_FAILED:
127             throw interactive_error("Download has already failed.");
128         case DOWNLOAD_CANCELED:
129             throw interactive_error("Download has already been canceled.");
130         case DOWNLOAD_PAUSED:
131             throw interactive_error("Download has already been paused.");
132         case DOWNLOAD_QUEUED:
133             throw interactive_error("Download is queued.");
134         default:
135             throw new Error("Download has unexpected state: " + this.state);
136         }
137     },
139     // Download manager operations
140     cancel : function ()  {
141         this.throw_if_removed();
142         switch (this.state) {
143         case DOWNLOAD_DOWNLOADING:
144         case DOWNLOAD_PAUSED:
145         case DOWNLOAD_QUEUED:
146             try {
147                 download_manager_service.cancelDownload(this.id);
148             } catch (e) {
149                 throw interactive_error("Download cannot be canceled.");
150             }
151             break;
152         default:
153             this.throw_state_error();
154         }
155     },
157     retry : function () {
158         this.throw_if_removed();
159         switch (this.state) {
160         case DOWNLOAD_CANCELED:
161         case DOWNLOAD_FAILED:
162             try {
163                 download_manager_service.retryDownload(this.id);
164             } catch (e) {
165                 throw interactive_error("Download cannot be retried.");
166             }
167             break;
168         default:
169             this.throw_state_error();
170         }
171     },
173     resume : function () {
174         this.throw_if_removed();
175         switch (this.state) {
176         case DOWNLOAD_PAUSED:
177             try {
178                 download_manager_service.resumeDownload(this.id);
179             } catch (e) {
180                 throw interactive_error("Download cannot be resumed.");
181             }
182             break;
183         default:
184             this.throw_state_error();
185         }
186     },
188     pause : function () {
189         this.throw_if_removed();
190         switch (this.state) {
191         case DOWNLOAD_DOWNLOADING:
192         case DOWNLOAD_QUEUED:
193             try {
194                 download_manager_service.pauseDownload(this.id);
195             } catch (e) {
196                 throw interactive_error("Download cannot be paused.");
197             }
198             break;
199         default:
200             this.throw_state_error();
201         }
202     },
204     remove : function () {
205         this.throw_if_removed();
206         switch (this.state) {
207         case DOWNLOAD_FAILED:
208         case DOWNLOAD_CANCELED:
209         case DOWNLOAD_FINISHED:
210             try {
211                 download_manager_service.removeDownload(this.id);
212             } catch (e) {
213                 throw interactive_error("Download cannot be removed.");
214             }
215             break;
216         default:
217             throw interactive_error("Download is still in progress.");
218         }
219     },
221     delete_target : function () {
222         if (this.state != DOWNLOAD_FINISHED)
223             throw interactive_error("Download has not finished.");
224         try {
225             this.target_file.remove(false);
226         } catch (e) {
227             if ("result" in e) {
228                 switch (e.result) {
229                 case Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST:
230                     throw interactive_error("File has already been deleted.");
231                 case Cr.NS_ERROR_FILE_ACCESS_DENIED:
232                     throw interactive_error("Access denied");
233                 case Cr.NS_ERROR_FILE_DIR_NOT_EMPTY:
234                     throw interactive_error("Failed to delete file.");
235                 }
236             }
237             throw e;
238         }
239     }
242 var define_download_local_hook = simple_local_hook_definer();
244 // FIXME: add more parameters
245 function register_download (buffer, source_uri, target_file) {
246     var info = new download_info(buffer, null, target_file);
247     info.registered_time_stamp = Date.now();
248     info.registered_source_uri = source_uri;
249     unmanaged_download_info_list.push(info);
250     return info;
253 function match_registered_download (mozilla_info) {
254     let list = unmanaged_download_info_list;
255     let t = Date.now();
256     for (let i = 0; i < list.length; ++i) {
257         let x = list[i];
258         if (x.registered_source_uri == mozilla_info.source) {
259             list.splice(i, 1);
260             return x;
261         }
262         if (t - x.registered_time_stamp > download_info_max_queue_delay) {
263             list.splice(i, 1);
264             --i;
265             continue;
266         }
267     }
268     return null;
271 define_download_local_hook("download_added_hook");
272 define_download_local_hook("download_removed_hook");
273 define_download_local_hook("download_finished_hook");
274 define_download_local_hook("download_progress_change_hook");
275 define_download_local_hook("download_state_change_hook");
276 define_download_local_hook("download_shell_command_change_hook");
278 define_variable('delete_temporary_files_for_command', true,
279     'If this is set to true, temporary files downloaded to run a command '+
280     'on them will be deleted once the command completes. If not, the file '+
281     'will stay around forever unless deleted outside the browser.');
283 var download_info_max_queue_delay = 100;
285 var download_progress_listener = {
286     QueryInterface: generate_QI(Ci.nsIDownloadProgressListener),
288     onDownloadStateChange : function (state, download) {
289         var info = null;
290         /* FIXME: Determine if only new downloads will have this state
291          * as their previous state. */
293         dumpln("download state change: " + download.source.spec + ": " + state + ", " + download.state + ", " + download.id);
295         if (state == DOWNLOAD_NOTSTARTED) {
296             info = match_registered_download(download);
297             if (info == null) {
298                 info = new download_info(null, download);
299                 dumpln("error: encountered unknown new download");
300             } else {
301                 info.attach(download);
302             }
303         } else {
304             info = id_to_download_info[download.id];
305             if (info == null) {
306                 dumpln("Error: encountered unknown download");
308             } else {
309                 info.mozilla_info = download;
310                 download_state_change_hook.run(info);
311                 if (info.state == DOWNLOAD_FINISHED) {
312                     download_finished_hook.run(info);
314                     if (info.shell_command != null) {
315                         info.running_shell_command = true;
316                         co_call(function () {
317                             try {
318                                 yield shell_command_with_argument(info.shell_command,
319                                                                   info.target_file.path,
320                                                                   $cwd = info.shell_command_cwd);
321                             } catch (e) {
322                                 handle_interactive_error(info.source_buffer.window, e);
323                             } finally  {
324                                 if (info.temporary_status == DOWNLOAD_TEMPORARY_FOR_COMMAND)
325                                     if(delete_temporary_files_for_command) {
326                                         info.target_file.remove(false /* not recursive */);
327                                     }
328                                 info.running_shell_command = false;
329                                 download_shell_command_change_hook.run(info);
330                             }
331                         }());
332                         download_shell_command_change_hook.run(info);
333                     }
334                 }
335             }
336         }
337     },
339     onProgressChange : function (progress, request, cur_self_progress, max_self_progress,
340                                  cur_total_progress, max_total_progress,
341                                  download) {
342         var info = id_to_download_info[download.id];
343         if (info == null) {
344             dumpln("error: encountered unknown download in progress change");
345             return;
346         }
347         info.mozilla_info = download;
348         download_progress_change_hook.run(info);
349         //dumpln("download progress change: " + download.source.spec + ": " + cur_self_progress + "/" + max_self_progress + " "
350         // + cur_total_progress + "/" + max_total_progress + ", " + download.state + ", " + download.id);
351     },
353     onSecurityChange : function (progress, request, state, download) {
354     },
356     onStateChange : function (progress, request, state_flags, status, download) {
357     }
360 var download_observer = {
361     observe : function (subject, topic, data) {
362         switch(topic) {
363         case "download-manager-remove-download":
364             var ids = [];
365             if (!subject) {
366                 // Remove all downloads
367                 for (let i in id_to_download_info)
368                     ids.push(i);
369             } else {
370                 let id = subject.QueryInterface(Ci.nsISupportsPRUint32);
371                 /* FIXME: determine if this should really be an error */
372                 if (!(id in id_to_download_info)) {
373                     dumpln("Error: download-manager-remove-download event received for unknown download: " + id);
374                 } else
375                     ids.push(id);
376             }
377             for each (let i in ids) {
378                 dumpln("deleting download: " + i);
379                 let d = id_to_download_info[i];
380                 d.removed = true;
381                 download_removed_hook.run(d);
382                 delete id_to_download_info[i];
383             }
384             break;
385         }
386     }
388 observer_service.addObserver(download_observer, "download-manager-remove-download", false);
390 download_manager_service.addListener(download_progress_listener);
392 function pretty_print_file_size (val) {
393     const GIBI = 1073741824; /* 2^30 */
394     const MEBI = 1048576; /* 2^20 */
395     const KIBI = 1024; /* 2^10 */
396     var suffix, div;
397     if (val < KIBI) {
398         div = 1;
399         suffix = "B";
400     }
401     else if (val < MEBI) {
402         suffix = "KiB";
403         div = KIBI;
404     } else if (val < GIBI) {
405         suffix = "MiB";
406         div = MEBI;
407     } else {
408         suffix = "GiB";
409         div = GIBI;
410     }
411     val = val / div;
412     var precision = 2;
413     if (val > 10)
414         precision = 1;
415     if (val > 100)
416         precision = 0;
417     return [val.toFixed(precision), suffix];
420 function pretty_print_time (val) {
421     val = Math.round(val);
422     var seconds = val % 60;
424     val = Math.floor(val / 60);
426     var minutes = val % 60;
428     var hours = Math.floor(val / 60);
430     var parts = [];
432     if (hours > 1)
433         parts.push(hours + " hours");
434     else if (hours == 1)
435         parts.push("1 hour");
437     if (minutes > 1)
438         parts.push(minutes + " minutes");
439     else if (minutes == 1)
440         parts.push("1 minute");
442     if (minutes <= 1 && hours == 0) {
443         if (seconds != 1)
444             parts.push(seconds + " seconds");
445         else
446             parts.push("1 second");
447     }
449     return parts.join(", ");
452 define_variable("download_buffer_min_update_interval", 2000,
453     "Minimum interval (in milliseconds) between updates in download progress buffers.\n" +
454     "Lowering this interval will increase the promptness of the progress display at " +
455     "the cost of using additional processor time.");
457 function download_buffer_modality (buffer, element) {
458     buffer.keymaps.push(download_buffer_keymap);
461 define_keywords("$info");
462 function download_buffer (window, element) {
463     this.constructor_begin();
464     keywords(arguments);
465     special_buffer.call(this, window, element, forward_keywords(arguments));
466     this.info = arguments.$info;
467     this.local.cwd = this.info.mozilla_info.targetFile.parent;
468     this.description = this.info.mozilla_info.source.spec;
469     this.update_title();
471     this.progress_change_handler_fn = method_caller(this, this.handle_progress_change);
472     add_hook.call(this.info, "download_progress_change_hook", this.progress_change_handler_fn);
473     add_hook.call(this.info, "download_state_change_hook", this.progress_change_handler_fn);
474     this.command_change_handler_fn = method_caller(this, this.update_command_field);
475     add_hook.call(this.info, "download_shell_command_change_hook", this.command_change_handler_fn);
476     this.modalities.push(download_buffer_modality);
477     this.constructor_end();
479 download_buffer.prototype = {
480     __proto__: special_buffer.prototype,
482     handle_kill : function () {
483         special_buffer.prototype.handle_kill.call(this);
484         remove_hook.call(this.info, "download_progress_change_hook", this.progress_change_handler_fn);
485         remove_hook.call(this.info, "download_state_change_hook", this.progress_change_handler_fn);
486         remove_hook.call(this.info, "download_shell_command_change_hook", this.command_change_handler_fn);
488         // Remove all node references
489         delete this.status_textnode;
490         delete this.target_file_node;
491         delete this.transferred_div_node;
492         delete this.transferred_textnode;
493         delete this.progress_container_node;
494         delete this.progress_bar_node;
495         delete this.percent_textnode;
496         delete this.time_textnode;
497         delete this.command_div_node;
498         delete this.command_label_textnode;
499         delete this.command_textnode;
500     },
502     update_title : function () {
503         // FIXME: do this properly
504         var new_title;
505         var info = this.info;
506         var append_transfer_info = false;
507         var append_speed_info = true;
508         var label = null;
509         switch(info.state) {
510         case DOWNLOAD_DOWNLOADING:
511             label = "Downloading";
512             append_transfer_info = true;
513             break;
514         case DOWNLOAD_FINISHED:
515             label = "Download complete";
516             break;
517         case DOWNLOAD_FAILED:
518             label = "Download failed";
519             append_transfer_info = true;
520             append_speed_info = false;
521             break;
522         case DOWNLOAD_CANCELED:
523             label = "Download canceled";
524             append_transfer_info = true;
525             append_speed_info = false;
526             break;
527         case DOWNLOAD_PAUSED:
528             label = "Download paused";
529             append_transfer_info = true;
530             append_speed_info = false;
531             break;
532         case DOWNLOAD_QUEUED:
533         default:
534             label = "Download queued";
535             break;
536         }
538         if (append_transfer_info) {
539             if (append_speed_info)
540                 new_title = label + " at " + pretty_print_file_size(info.speed).join(" ") + "/s: ";
541             else
542                 new_title = label + ": ";
543             var trans = pretty_print_file_size(info.amount_transferred);
544             if (info.size >= 0) {
545                 var total = pretty_print_file_size(info.size);
546                 if (trans[1] == total[1])
547                     new_title += trans[0] + "/" + total[0] + " " + total[1];
548                 else
549                     new_title += trans.join(" ") + "/" + total.join(" ");
550             } else
551                 new_title += trans.join(" ");
552             if (info.percent_complete >= 0)
553                 new_title += " (" + info.percent_complete + "%)";
554         } else
555             new_title = label;
556         if (new_title != this.title) {
557             this.title = new_title;
558             return true;
559         }
560         return false;
561     },
563     handle_progress_change : function () {
564         var cur_time = Date.now();
565         if (this.last_update == null ||
566             (cur_time - this.last_update) > download_buffer_min_update_interval ||
567             this.info.state != this.previous_state) {
569             if (this.update_title())
570                 buffer_title_change_hook.run(this);
572             if (this.generated) {
573                 this.update_fields();
574             }
575             this.previous_status = this.info.status;
576             this.last_update = cur_time;
577         }
578     },
580     generate : function () {
581         var d = this.document;
582         var g = new dom_generator(d, XHTML_NS);
584         /* Warning: If any additional node references are saved in
585          * this function, appropriate code to delete the saved
586          * properties must be added to handle_kill. */
588         var info = this.info;
590         d.body.setAttribute("class", "download-buffer");
592         g.add_stylesheet("chrome://conkeror-gui/content/downloads.css");
594         var row, cell;
595         var table = g.element("table", d.body);
597         row = g.element("tr", table, "class", "download-info", "id", "download-source");
598         cell = g.element("td", row, "class", "download-label");
599         this.status_textnode = g.text("", cell);
600         cell = g.element("td", row, "class", "download-value");
601         g.text(info.source.spec, cell);
603         row = g.element("tr", table, "class", "download-info", "id", "download-target");
604         cell = g.element("td", row, "class", "download-label");
605         var target_label;
606         if (info.temporary_status != DOWNLOAD_NOT_TEMPORARY)
607             target_label = "Temp. file:";
608         else
609             target_label = "Target:";
610         g.text(target_label, cell);
611         cell = g.element("td", row, "class", "download-value");
612         this.target_file_node = g.text("", cell);
614         row = g.element("tr", table, "class", "download-info", "id", "download-mime-type");
615         cell = g.element("td", row, "class", "download-label");
616         g.text("MIME type:", cell);
617         cell = g.element("td", row, "class", "download-value");
618         g.text(info.MIME_type || "unknown", cell);
620         this.transferred_div_node = row =
621             g.element("tr", table, "class", "download-info", "id", "download-transferred");
622         cell = g.element("td", row, "class", "download-label");
623         g.text("Transferred:", cell);
624         cell = g.element("td", row, "class", "download-value");
625         var sub_item = g.element("div", cell);
626         this.transferred_textnode = g.text("", sub_item);
627         sub_item = g.element("div", cell, "id", "download-percent");
628         this.percent_textnode = g.text("", sub_item);
629         this.progress_container_node = sub_item = g.element("div", cell, "id", "download-progress-container");
630         this.progress_bar_node = g.element("div", sub_item, "id", "download-progress-bar");
632         row = g.element("tr", table, "class", "download-info", "id", "download-time");
633         cell = g.element("td", row, "class", "download-label");
634         g.text("Time:", cell);
635         cell = g.element("td", row, "class", "download-value");
636         this.time_textnode = g.text("", cell);
638         if (info.action_description != null) {
639             row = g.element("tr", table, "class", "download-info", "id", "download-action");
640             cell = g.element("div", row, "class", "download-label");
641             g.text("Action:", cell);
642             cell = g.element("div", row, "class", "download-value");
643             g.text(info.action_description, cell);
644        }
646         this.command_div_node = row = g.element("tr", table, "class", "download-info", "id", "download-command");
647         cell = g.element("td", row, "class", "download-label");
648         this.command_label_textnode = g.text("Run command:", cell);
649         cell = g.element("td", row, "class", "download-value");
650         this.command_textnode = g.text("", cell);
653         this.update_fields();
654         this.update_command_field();
655     },
657     update_fields : function () {
658         if (!this.generated)
659             return;
660         var info = this.info;
661         var label = null;
662         switch(info.state) {
663         case DOWNLOAD_DOWNLOADING:
664             label = "Downloading";
665             break;
666         case DOWNLOAD_FINISHED:
667             label = "Completed";
668             break;
669         case DOWNLOAD_FAILED:
670             label = "Failed";
671             break;
672         case DOWNLOAD_CANCELED:
673             label = "Canceled";
674             break;
675         case DOWNLOAD_PAUSED:
676             label = "Paused";
677             break;
678         case DOWNLOAD_QUEUED:
679         default:
680             label = "Queued";
681             break;
682         }
683         this.status_textnode.nodeValue = label + ":";
684         this.target_file_node.nodeValue = info.target_file_text();
685         this.update_time_field();
687         var tran_text = "";
688         if (info.state == DOWNLOAD_FINISHED)
689             tran_text = pretty_print_file_size(info.size).join(" ");
690         else {
691             var trans = pretty_print_file_size(info.amount_transferred);
692             if (info.size >= 0) {
693                 var total = pretty_print_file_size(info.size);
694                 if (trans[1] == total[1])
695                     tran_text += trans[0] + "/" + total[0] + " " + total[1];
696                 else
697                     tran_text += trans.join(" ") + "/" + total.join(" ");
698             } else
699                 tran_text += trans.join(" ");
700         }
701         this.transferred_textnode.nodeValue = tran_text;
702         if (info.percent_complete >= 0) {
703             this.progress_container_node.style.display = "";
704             this.percent_textnode.nodeValue = info.percent_complete + "%";
705             this.progress_bar_node.style.width = info.percent_complete + "%";
706         } else {
707             this.percent_textnode.nodeValue = "";
708             this.progress_container_node.style.display = "none";
709         }
711         this.update_command_field();
712     },
714     update_time_field : function () {
715         var info = this.info;
716         var elapsed_text = pretty_print_time((Date.now() - info.start_time / 1000) / 1000) + " elapsed";
717         var text = "";
718         if (info.state == DOWNLOAD_DOWNLOADING) {
719             text = pretty_print_file_size(info.speed).join(" ") + "/s, ";
720         }
721         if (info.state == DOWNLOAD_DOWNLOADING &&
722             info.size >= 0 &&
723             info.speed > 0) {
724             let remaining = (info.size - info.amount_transferred) / info.speed;
725             text += pretty_print_time(remaining) + " left (" + elapsed_text + ")";
726         } else {
727             text = elapsed_text;
728         }
729         this.time_textnode.nodeValue = text;
730     },
732     update_command_field : function () {
733         if (!this.generated)
734             return;
735         if (this.info.shell_command != null) {
736             this.command_div_node.style.display = "";
737             var label;
738             if (this.info.running_shell_command)
739                 label = "Running:";
740             else if (this.info.state == DOWNLOAD_FINISHED)
741                 label = "Ran command:";
742             else
743                 label = "Run command:";
744             this.command_label_textnode.nodeValue = label;
745             this.command_textnode.nodeValue = this.info.shell_command;
746         } else {
747             this.command_div_node.style.display = "none";
748         }
749     }
752 function download_cancel (buffer) {
753     check_buffer(buffer, download_buffer);
754     var info = buffer.info;
755     info.cancel();
756     buffer.window.minibuffer.message("Download canceled");
758 interactive("download-cancel",
759             "Cancel the current download.\n" +
760             "The download can later be retried using the `download-retry' command, but any " +
761             "data already transferred will be lost.",
762             function (I) {
763                 let result = yield I.window.minibuffer.read_single_character_option(
764                     $prompt = "Cancel this download? (y/n)",
765                     $options = ["y", "n"]);
766                 if (result == "y")
767                     download_cancel(I.buffer);
768             });
770 function download_retry (buffer) {
771     check_buffer(buffer, download_buffer);
772     var info = buffer.info;
773     info.retry();
774     buffer.window.minibuffer.message("Download retried");
776 interactive("download-retry",
777             "Retry a failed or canceled download.\n" +
778             "This command can be used to retry a download that failed or was canceled using " +
779             "the `download-cancel' command.  The download will begin from the start again.",
780             function (I) {download_retry(I.buffer);});
782 function download_pause (buffer) {
783     check_buffer(buffer, download_buffer);
784     buffer.info.pause();
785     buffer.window.minibuffer.message("Download paused");
787 interactive("download-pause",
788             "Pause the current download.\n" +
789             "The download can later be resumed using the `download-resume' command.  The " +
790             "data already transferred will not be lost.",
791             function (I) {download_pause(I.buffer);});
793 function download_resume (buffer) {
794     check_buffer(buffer, download_buffer);
795     buffer.info.resume();
796     buffer.window.minibuffer.message("Download resumed");
798 interactive("download-resume",
799             "Resume the current download.\n" +
800             "This command can be used to resume a download paused using the `download-pause' command.",
801             function (I) { download_resume(I.buffer); });
803 function download_remove (buffer) {
804     check_buffer(buffer, download_buffer);
805     buffer.info.remove();
806     buffer.window.minibuffer.message("Download removed");
808 interactive("download-remove",
809             "Remove the current download from the download manager.\n" +
810             "This command can only be used on inactive (paused, canceled, "+
811             "completed, or failed) downloads.",
812             function (I) {download_remove(I.buffer);});
814 function download_retry_or_resume (buffer) {
815     check_buffer(buffer, download_buffer);
816     var info = buffer.info;
817     if (info.state == DOWNLOAD_PAUSED)
818         download_resume(buffer);
819     else
820         download_retry(buffer);
822 interactive("download-retry-or-resume",
823             "Retry or resume the current download.\n" +
824             "This command can be used to resume a download paused using the `download-pause' " +
825             "command or canceled using the `download-cancel' command.",
826             function (I) {download_retry_or_resume(I.buffer);});
828 function download_pause_or_resume (buffer) {
829     check_buffer(buffer, download_buffer);
830     var info = buffer.info;
831     if (info.state == DOWNLOAD_PAUSED)
832         download_resume(buffer);
833     else
834         download_pause(buffer);
836 interactive("download-pause-or-resume",
837             "Pause or resume the current download.\n" +
838             "This command toggles the paused state of the current download.",
839             function (I) {download_pause_or_resume(I.buffer);});
841 function download_delete_target (buffer) {
842     check_buffer(buffer, download_buffer);
843     var info = buffer.info;
844     info.delete_target();
845     buffer.window.minibuffer.message("Deleted file: " + info.target_file.path);
847 interactive("download-delete-target",
848             "Delete the target file of the current download.\n"  +
849             "This command can only be used if the download has finished successfully.",
850             function (I) {download_delete_target(I.buffer);});
852 function download_shell_command (buffer, cwd, cmd) {
853     check_buffer(buffer, download_buffer);
854     var info = buffer.info;
855     if (info.state == DOWNLOAD_FINISHED) {
856         shell_command_with_argument_blind(cmd, info.target_file.path, $cwd = cwd);
857         return;
858     }
859     if (info.state != DOWNLOAD_DOWNLOADING && info.state != DOWNLOAD_PAUSED && info.state != DOWNLOAD_QUEUED)
860         info.throw_state_error();
861     if (cmd == null || cmd.length == 0)
862         info.set_shell_command(null, cwd);
863     else
864         info.set_shell_command(cmd, cwd);
865     buffer.window.minibuffer.message("Queued shell command: " + cmd);
867 interactive("download-shell-command",
868             "Run a shell command on the target file of the current download.\n" +
869             "If the download is still in progress, the shell command will be queued " +
870             "to run when the download finishes.",
871             function (I) {
872                 var buffer = check_buffer(I.buffer, download_buffer);
873                 var cwd = buffer.info.shell_command_cwd || I.local.cwd;
874                 var cmd = yield I.minibuffer.read_shell_command(
875                     $cwd = cwd,
876                     $initial_value = buffer.info.shell_command ||
877                         external_content_handlers.get(buffer.info.MIME_type));
878                 download_shell_command(buffer, cwd, cmd);
879             });
881 function download_manager_ui () {}
882 download_manager_ui.prototype = {
883     QueryInterface : XPCOMUtils.generateQI([Ci.nsIDownloadManagerUI]),
885     getAttention : function () {},
886     show : function () {},
887     visible : false
891 function download_manager_show_builtin_ui (window) {
892     download_manager_builtin_ui.show(window);
894 interactive("download-manager-show-builtin-ui",
895             "Show the built-in (Firefox-style) download manager user interface.",
896             function (I) {download_manager_show_builtin_ui(I.window);});
899 define_variable("download_temporary_file_open_buffer_delay", 500,
900     "Delay (in milliseconds) before a download buffer is opened for "+
901     "temporary downloads.  If the download completes before this amount "+
902     "of time, no download buffer will be opened.  This variable takes "+
903     "effect only if `open_download_buffer_automatically' is in "+
904     "`download_added_hook', which is the case by default.");
907 define_variable("download_buffer_automatic_open_target",
908                 [OPEN_NEW_WINDOW, OPEN_NEW_BUFFER_BACKGROUND],
909     "Target(s) for download buffers created by "+
910     "`open_download_buffer_automatically' and `download-show'.\n"+
911     "It can be a single target or an array of two targets.  When it is an "+
912     "array, the `download-show' command will use the second target when "+
913     "called with universal-argument.");
916 function open_download_buffer_automatically (info, target) {
917     var buf = info.source_buffer;
918     if (target == null) {
919         if (typeof(download_buffer_automatic_open_target) == "object")
920             target = download_buffer_automatic_open_target[0];
921         else
922             target = download_buffer_automatic_open_target;
923     }
924     if (buf == null)
925         target = OPEN_NEW_WINDOW;
926     if (info.temporary_status == DOWNLOAD_NOT_TEMPORARY ||
927         download_temporary_file_open_buffer_delay == 0)
928     {
929         create_buffer(buf.window, buffer_creator(download_buffer, $info = info), target);
930     } else {
931         var timer = null;
932         function finish () {
933             timer.cancel();
934         }
935         add_hook.call(info, "download_finished_hook", finish);
936         timer = call_after_timeout(function () {
937                 remove_hook.call(info, "download_finished_hook", finish);
938                 create_buffer(buf.window, buffer_creator(download_buffer, $info = info), target);
939             }, download_temporary_file_open_buffer_delay);
940     }
942 add_hook("download_added_hook", open_download_buffer_automatically);
946  * Download-show
947  */ 
949 minibuffer_auto_complete_preferences.download = true;
951 minibuffer.prototype.read_download = function () {
952     keywords(arguments,
953              $prompt = "Download",
954              $completer = all_word_completer(
955                  $completions = function (visitor) {
956                      var dls = download_manager_service.activeDownloads;
957                      while (dls.hasMoreElements()) {
958                          let dl = dls.getNext();
959                          visitor(id_to_download_info[dl.id]);
960                      }
961                  },
962                  $get_string = function (x) x.display_name,
963                  $get_description = function (x) x.source.spec,
964                  $get_value = function (x) x),
965              $auto_complete = "download",
966              $auto_complete_initial = true,
967              $match_required = true);
968     var result = yield this.read(forward_keywords(arguments));
969     yield co_return(result);
972 interactive("download-show",
973     "Prompt for an ongoing download and open a download buffer showing "+
974     "its progress.  When called with universal argument, the second "+
975     "target from `download_buffer_automatic_open_target' will be used.",
976     function (I) {
977         var target = null;
978         if (I.P && typeof(download_buffer_automatic_open_target) == "object")
979             target = download_buffer_automatic_open_target[1];
980         open_download_buffer_automatically(
981             (yield I.minibuffer.read_download($prompt = "Show download:")),
982             target);
983     });