Debian package: Support xulrunner 9+10 in debian/conkeror.bin, drop support for unver...
[conkeror.git] / modules / download-manager.js
blob4af35d8e0abe36b4173917725c04f0dde0f3a4c0
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 in_module(null);
11 require("special-buffer.js");
12 require("mime-type-override.js");
13 require("minibuffer-read-mime-type.js");
15 var download_manager_service = Cc["@mozilla.org/download-manager;1"]
16     .getService(Ci.nsIDownloadManager);
18 var unmanaged_download_info_list = [];
19 var id_to_download_info = {};
21 // Import these constants for convenience
22 const DOWNLOAD_NOTSTARTED = Ci.nsIDownloadManager.DOWNLOAD_NOTSTARTED;
23 const DOWNLOAD_DOWNLOADING = Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING;
24 const DOWNLOAD_FINISHED = Ci.nsIDownloadManager.DOWNLOAD_FINISHED;
25 const DOWNLOAD_FAILED = Ci.nsIDownloadManager.DOWNLOAD_FAILED;
26 const DOWNLOAD_CANCELED = Ci.nsIDownloadManager.DOWNLOAD_CANCELED;
27 const DOWNLOAD_PAUSED = Ci.nsIDownloadManager.DOWNLOAD_PAUSED;
28 const DOWNLOAD_QUEUED = Ci.nsIDownloadManager.DOWNLOAD_QUEUED;
29 const DOWNLOAD_BLOCKED = Ci.nsIDownloadManager.DOWNLOAD_BLOCKED;
30 const DOWNLOAD_SCANNING = Ci.nsIDownloadManager.DOWNLOAD_SCANNING;
33 const DOWNLOAD_NOT_TEMPORARY = 0;
34 const DOWNLOAD_TEMPORARY_FOR_ACTION = 1;
35 const DOWNLOAD_TEMPORARY_FOR_COMMAND = 2;
37 function download_info (source_buffer, mozilla_info, target_file) {
38     this.source_buffer = source_buffer;
39     this.target_file = target_file;
40     if (mozilla_info != null)
41         this.attach(mozilla_info);
43 download_info.prototype = {
44     constructor: download_info,
45     attach: function (mozilla_info) {
46         if (!this.target_file)
47             this.__defineGetter__("target_file", function () {
48                     return this.mozilla_info.targetFile;
49                 });
50         else if (this.target_file.path != mozilla_info.targetFile.path)
51             throw interactive_error("Download target file unexpected.");
52         this.mozilla_info = mozilla_info;
53         id_to_download_info[mozilla_info.id] = this;
54         download_added_hook.run(this);
55     },
56     target_file: null,
57     shell_command: null,
58     shell_command_cwd: null,
59     temporary_status: DOWNLOAD_NOT_TEMPORARY,
60     action_description: null,
61     set_shell_command: function (str, cwd) {
62         this.shell_command = str;
63         this.shell_command_cwd = cwd;
64         if (this.mozilla_info)
65             download_shell_command_change_hook.run(this);
66     },
68     /**
69      * None of the following members may be used until attach is called
70      */
72     // Reflectors to properties of nsIDownload
73     get state () { return this.mozilla_info.state; },
74     get display_name () { return this.mozilla_info.displayName; },
75     get amount_transferred () { return this.mozilla_info.amountTransferred; },
76     get percent_complete () { return this.mozilla_info.percentComplete; },
77     get size () {
78         var s = this.mozilla_info.size;
79         /* nsIDownload.size is a PRUint64, and will have value
80          * LL_MAXUINT (2^64 - 1) to indicate an unknown size.  Because
81          * JavaScript only has a double numerical type, this value
82          * cannot be represented exactly, so 2^36 is used instead as the cutoff. */
83         if (s < 68719476736 /* 2^36 */)
84             return s;
85         return -1;
86     },
87     get source () { return this.mozilla_info.source; },
88     get start_time () { return this.mozilla_info.startTime; },
89     get speed () { return this.mozilla_info.speed; },
90     get MIME_info () { return this.mozilla_info.MIMEInfo; },
91     get MIME_type () {
92         if (this.MIME_info)
93             return this.MIME_info.MIMEType;
94         return null;
95     },
96     get id () { return this.mozilla_info.id; },
97     get referrer () { return this.mozilla_info.referrer; },
99     target_file_text: function () {
100         let target = this.target_file.path;
101         let display = this.display_name;
102         if (target.indexOf(display, target.length - display.length) == -1)
103             target += " (" + display + ")";
104         return target;
105     },
107     throw_if_removed: function () {
108         if (this.removed)
109             throw interactive_error("Download has already been removed from the download manager.");
110     },
112     throw_state_error: function () {
113         switch (this.state) {
114         case DOWNLOAD_DOWNLOADING:
115             throw interactive_error("Download is already in progress.");
116         case DOWNLOAD_FINISHED:
117             throw interactive_error("Download has already completed.");
118         case DOWNLOAD_FAILED:
119             throw interactive_error("Download has already failed.");
120         case DOWNLOAD_CANCELED:
121             throw interactive_error("Download has already been canceled.");
122         case DOWNLOAD_PAUSED:
123             throw interactive_error("Download has already been paused.");
124         case DOWNLOAD_QUEUED:
125             throw interactive_error("Download is queued.");
126         default:
127             throw new Error("Download has unexpected state: " + this.state);
128         }
129     },
131     // Download manager operations
132     cancel: function ()  {
133         this.throw_if_removed();
134         switch (this.state) {
135         case DOWNLOAD_DOWNLOADING:
136         case DOWNLOAD_PAUSED:
137         case DOWNLOAD_QUEUED:
138             try {
139                 download_manager_service.cancelDownload(this.id);
140             } catch (e) {
141                 throw interactive_error("Download cannot be canceled.");
142             }
143             break;
144         default:
145             this.throw_state_error();
146         }
147     },
149     retry: function () {
150         this.throw_if_removed();
151         switch (this.state) {
152         case DOWNLOAD_CANCELED:
153         case DOWNLOAD_FAILED:
154             try {
155                 download_manager_service.retryDownload(this.id);
156             } catch (e) {
157                 throw interactive_error("Download cannot be retried.");
158             }
159             break;
160         default:
161             this.throw_state_error();
162         }
163     },
165     resume: function () {
166         this.throw_if_removed();
167         switch (this.state) {
168         case DOWNLOAD_PAUSED:
169             try {
170                 download_manager_service.resumeDownload(this.id);
171             } catch (e) {
172                 throw interactive_error("Download cannot be resumed.");
173             }
174             break;
175         default:
176             this.throw_state_error();
177         }
178     },
180     pause: function () {
181         this.throw_if_removed();
182         switch (this.state) {
183         case DOWNLOAD_DOWNLOADING:
184         case DOWNLOAD_QUEUED:
185             try {
186                 download_manager_service.pauseDownload(this.id);
187             } catch (e) {
188                 throw interactive_error("Download cannot be paused.");
189             }
190             break;
191         default:
192             this.throw_state_error();
193         }
194     },
196     remove: function () {
197         this.throw_if_removed();
198         switch (this.state) {
199         case DOWNLOAD_FAILED:
200         case DOWNLOAD_CANCELED:
201         case DOWNLOAD_FINISHED:
202             try {
203                 download_manager_service.removeDownload(this.id);
204             } catch (e) {
205                 throw interactive_error("Download cannot be removed.");
206             }
207             break;
208         default:
209             throw interactive_error("Download is still in progress.");
210         }
211     },
213     delete_target: function () {
214         if (this.state != DOWNLOAD_FINISHED)
215             throw interactive_error("Download has not finished.");
216         try {
217             this.target_file.remove(false);
218         } catch (e) {
219             if ("result" in e) {
220                 switch (e.result) {
221                 case Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST:
222                     throw interactive_error("File has already been deleted.");
223                 case Cr.NS_ERROR_FILE_ACCESS_DENIED:
224                     throw interactive_error("Access denied");
225                 case Cr.NS_ERROR_FILE_DIR_NOT_EMPTY:
226                     throw interactive_error("Failed to delete file.");
227                 }
228             }
229             throw e;
230         }
231     }
234 var define_download_local_hook = simple_local_hook_definer();
236 function register_download (buffer, source_uri, target_file) {
237     var info = new download_info(buffer, null, target_file);
238     info.registered_time_stamp = Date.now();
239     info.registered_source_uri = source_uri;
240     unmanaged_download_info_list.push(info);
241     return info;
244 function match_registered_download (mozilla_info) {
245     let list = unmanaged_download_info_list;
246     let t = Date.now();
247     for (let i = 0; i < list.length; ++i) {
248         let x = list[i];
249         if (x.registered_source_uri == mozilla_info.source) {
250             list.splice(i, 1);
251             return x;
252         }
253         if (t - x.registered_time_stamp > download_info_max_queue_delay) {
254             list.splice(i, 1);
255             --i;
256             continue;
257         }
258     }
259     return null;
262 define_download_local_hook("download_added_hook");
263 define_download_local_hook("download_removed_hook");
264 define_download_local_hook("download_finished_hook");
265 define_download_local_hook("download_progress_change_hook");
266 define_download_local_hook("download_state_change_hook");
267 define_download_local_hook("download_shell_command_change_hook");
269 define_variable('delete_temporary_files_for_command', true,
270     'If this is set to true, temporary files downloaded to run a command '+
271     'on them will be deleted once the command completes. If not, the file '+
272     'will stay around forever unless deleted outside the browser.');
274 var download_info_max_queue_delay = 100;
276 var download_progress_listener = {
277     QueryInterface: generate_QI(Ci.nsIDownloadProgressListener),
279     onDownloadStateChange: function (state, download) {
280         var info = null;
281         /* FIXME: Determine if only new downloads will have this state
282          * as their previous state. */
284         dumpln("download state change: " + download.source.spec + ": " + state + ", " + download.state + ", " + download.id);
286         if (state == DOWNLOAD_NOTSTARTED) {
287             info = match_registered_download(download);
288             if (info == null) {
289                 info = new download_info(null, download);
290                 dumpln("error: encountered unknown new download");
291             } else {
292                 info.attach(download);
293             }
294         } else {
295             info = id_to_download_info[download.id];
296             if (info == null) {
297                 dumpln("Error: encountered unknown download");
299             } else {
300                 info.mozilla_info = download;
301                 download_state_change_hook.run(info);
302                 if (info.state == DOWNLOAD_FINISHED) {
303                     download_finished_hook.run(info);
305                     if (info.shell_command != null) {
306                         info.running_shell_command = true;
307                         co_call(function () {
308                             try {
309                                 yield shell_command_with_argument(info.shell_command,
310                                                                   info.target_file.path,
311                                                                   $cwd = info.shell_command_cwd);
312                             } catch (e) {
313                                 handle_interactive_error(info.source_buffer.window, e);
314                             } finally  {
315                                 if (info.temporary_status == DOWNLOAD_TEMPORARY_FOR_COMMAND)
316                                     if(delete_temporary_files_for_command) {
317                                         info.target_file.remove(false /* not recursive */);
318                                     }
319                                 info.running_shell_command = false;
320                                 download_shell_command_change_hook.run(info);
321                             }
322                         }());
323                         download_shell_command_change_hook.run(info);
324                     }
325                 }
326             }
327         }
328     },
330     onProgressChange: function (progress, request, cur_self_progress, max_self_progress,
331                                 cur_total_progress, max_total_progress,
332                                 download) {
333         var info = id_to_download_info[download.id];
334         if (info == null) {
335             dumpln("error: encountered unknown download in progress change");
336             return;
337         }
338         info.mozilla_info = download;
339         download_progress_change_hook.run(info);
340         //dumpln("download progress change: " + download.source.spec + ": " + cur_self_progress + "/" + max_self_progress + " "
341         // + cur_total_progress + "/" + max_total_progress + ", " + download.state + ", " + download.id);
342     },
344     onSecurityChange: function (progress, request, state, download) {
345     },
347     onStateChange: function (progress, request, state_flags, status, download) {
348     }
351 var download_observer = {
352     observe: function (subject, topic, data) {
353         switch(topic) {
354         case "download-manager-remove-download":
355             var ids = [];
356             if (!subject) {
357                 // Remove all downloads
358                 for (let i in id_to_download_info)
359                     ids.push(i);
360             } else {
361                 let id = subject.QueryInterface(Ci.nsISupportsPRUint32);
362                 /* FIXME: determine if this should really be an error */
363                 if (!(id in id_to_download_info)) {
364                     dumpln("Error: download-manager-remove-download event received for unknown download: " + id);
365                 } else
366                     ids.push(id);
367             }
368             for each (let i in ids) {
369                 dumpln("deleting download: " + i);
370                 let d = id_to_download_info[i];
371                 d.removed = true;
372                 download_removed_hook.run(d);
373                 delete id_to_download_info[i];
374             }
375             break;
376         }
377     }
379 observer_service.addObserver(download_observer, "download-manager-remove-download", false);
381 download_manager_service.addListener(download_progress_listener);
383 define_variable("download_buffer_min_update_interval", 2000,
384     "Minimum interval (in milliseconds) between updates in download progress buffers.\n" +
385     "Lowering this interval will increase the promptness of the progress display at " +
386     "the cost of using additional processor time.");
388 function download_buffer_modality (buffer, element) {
389     buffer.keymaps.push(download_buffer_keymap);
392 define_keywords("$info");
393 function download_buffer (window) {
394     this.constructor_begin();
395     keywords(arguments);
396     special_buffer.call(this, window, forward_keywords(arguments));
397     this.info = arguments.$info;
398     this.local.cwd = this.info.mozilla_info.targetFile.parent;
399     this.description = this.info.mozilla_info.source.spec;
400     this.update_title();
402     this.progress_change_handler_fn = method_caller(this, this.handle_progress_change);
403     add_hook.call(this.info, "download_progress_change_hook", this.progress_change_handler_fn);
404     add_hook.call(this.info, "download_state_change_hook", this.progress_change_handler_fn);
405     this.command_change_handler_fn = method_caller(this, this.update_command_field);
406     add_hook.call(this.info, "download_shell_command_change_hook", this.command_change_handler_fn);
407     this.modalities.push(download_buffer_modality);
408     this.constructor_end();
410 download_buffer.prototype = {
411     constructor: download_buffer,
412     __proto__: special_buffer.prototype,
414     destroy: function () {
415         remove_hook.call(this.info, "download_progress_change_hook", this.progress_change_handler_fn);
416         remove_hook.call(this.info, "download_state_change_hook", this.progress_change_handler_fn);
417         remove_hook.call(this.info, "download_shell_command_change_hook", this.command_change_handler_fn);
419         // Remove all node references
420         delete this.status_textnode;
421         delete this.target_file_node;
422         delete this.transferred_div_node;
423         delete this.transferred_textnode;
424         delete this.progress_container_node;
425         delete this.progress_bar_node;
426         delete this.percent_textnode;
427         delete this.time_textnode;
428         delete this.command_div_node;
429         delete this.command_label_textnode;
430         delete this.command_textnode;
432         special_buffer.prototype.destroy.call(this);
433     },
435     update_title: function () {
436         // FIXME: do this properly
437         var new_title;
438         var info = this.info;
439         var append_transfer_info = false;
440         var append_speed_info = true;
441         var label = null;
442         switch(info.state) {
443         case DOWNLOAD_DOWNLOADING:
444             label = "Downloading";
445             append_transfer_info = true;
446             break;
447         case DOWNLOAD_FINISHED:
448             label = "Download complete";
449             break;
450         case DOWNLOAD_FAILED:
451             label = "Download failed";
452             append_transfer_info = true;
453             append_speed_info = false;
454             break;
455         case DOWNLOAD_CANCELED:
456             label = "Download canceled";
457             append_transfer_info = true;
458             append_speed_info = false;
459             break;
460         case DOWNLOAD_PAUSED:
461             label = "Download paused";
462             append_transfer_info = true;
463             append_speed_info = false;
464             break;
465         case DOWNLOAD_QUEUED:
466         default:
467             label = "Download queued";
468             break;
469         }
471         if (append_transfer_info) {
472             if (append_speed_info)
473                 new_title = label + " at " + pretty_print_file_size(info.speed).join(" ") + "/s: ";
474             else
475                 new_title = label + ": ";
476             var trans = pretty_print_file_size(info.amount_transferred);
477             if (info.size >= 0) {
478                 var total = pretty_print_file_size(info.size);
479                 if (trans[1] == total[1])
480                     new_title += trans[0] + "/" + total[0] + " " + total[1];
481                 else
482                     new_title += trans.join(" ") + "/" + total.join(" ");
483             } else
484                 new_title += trans.join(" ");
485             if (info.percent_complete >= 0)
486                 new_title += " (" + info.percent_complete + "%)";
487         } else
488             new_title = label;
489         if (new_title != this.title) {
490             this.title = new_title;
491             return true;
492         }
493         return false;
494     },
496     handle_progress_change: function () {
497         var cur_time = Date.now();
498         if (this.last_update == null ||
499             (cur_time - this.last_update) > download_buffer_min_update_interval ||
500             this.info.state != this.previous_state) {
502             if (this.update_title())
503                 buffer_title_change_hook.run(this);
505             if (this.generated) {
506                 this.update_fields();
507             }
508             this.previous_status = this.info.status;
509             this.last_update = cur_time;
510         }
511     },
513     generate: function () {
514         var d = this.document;
515         var g = new dom_generator(d, XHTML_NS);
517         /* Warning: If any additional node references are saved in
518          * this function, appropriate code to delete the saved
519          * properties must be added to destroy method. */
521         var info = this.info;
523         d.body.setAttribute("class", "download-buffer");
525         g.add_stylesheet("chrome://conkeror-gui/content/downloads.css");
527         var row, cell;
528         var table = g.element("table", d.body);
530         row = g.element("tr", table, "class", "download-info", "id", "download-source");
531         cell = g.element("td", row, "class", "download-label");
532         this.status_textnode = g.text("", cell);
533         cell = g.element("td", row, "class", "download-value");
534         g.text(info.source.spec, cell);
536         row = g.element("tr", table, "class", "download-info", "id", "download-target");
537         cell = g.element("td", row, "class", "download-label");
538         var target_label;
539         if (info.temporary_status != DOWNLOAD_NOT_TEMPORARY)
540             target_label = "Temp. file:";
541         else
542             target_label = "Target:";
543         g.text(target_label, cell);
544         cell = g.element("td", row, "class", "download-value");
545         this.target_file_node = g.text("", cell);
547         row = g.element("tr", table, "class", "download-info", "id", "download-mime-type");
548         cell = g.element("td", row, "class", "download-label");
549         g.text("MIME type:", cell);
550         cell = g.element("td", row, "class", "download-value");
551         g.text(info.MIME_type || "unknown", cell);
553         this.transferred_div_node = row =
554             g.element("tr", table, "class", "download-info", "id", "download-transferred");
555         cell = g.element("td", row, "class", "download-label");
556         g.text("Transferred:", cell);
557         cell = g.element("td", row, "class", "download-value");
558         var sub_item = g.element("div", cell);
559         this.transferred_textnode = g.text("", sub_item);
560         sub_item = g.element("div", cell, "id", "download-percent");
561         this.percent_textnode = g.text("", sub_item);
562         this.progress_container_node = sub_item = g.element("div", cell, "id", "download-progress-container");
563         this.progress_bar_node = g.element("div", sub_item, "id", "download-progress-bar");
565         row = g.element("tr", table, "class", "download-info", "id", "download-time");
566         cell = g.element("td", row, "class", "download-label");
567         g.text("Time:", cell);
568         cell = g.element("td", row, "class", "download-value");
569         this.time_textnode = g.text("", cell);
571         if (info.action_description != null) {
572             row = g.element("tr", table, "class", "download-info", "id", "download-action");
573             cell = g.element("div", row, "class", "download-label");
574             g.text("Action:", cell);
575             cell = g.element("div", row, "class", "download-value");
576             g.text(info.action_description, cell);
577         }
579         this.command_div_node = row = g.element("tr", table, "class", "download-info", "id", "download-command");
580         cell = g.element("td", row, "class", "download-label");
581         this.command_label_textnode = g.text("Run command:", cell);
582         cell = g.element("td", row, "class", "download-value");
583         this.command_textnode = g.text("", cell);
585         this.update_fields();
586         this.update_command_field();
587     },
589     update_fields: function () {
590         if (!this.generated)
591             return;
592         var info = this.info;
593         var label = null;
594         switch (info.state) {
595         case DOWNLOAD_DOWNLOADING:
596             label = "Downloading";
597             break;
598         case DOWNLOAD_FINISHED:
599             label = "Completed";
600             break;
601         case DOWNLOAD_FAILED:
602             label = "Failed";
603             break;
604         case DOWNLOAD_CANCELED:
605             label = "Canceled";
606             break;
607         case DOWNLOAD_PAUSED:
608             label = "Paused";
609             break;
610         case DOWNLOAD_QUEUED:
611         default:
612             label = "Queued";
613             break;
614         }
615         this.status_textnode.nodeValue = label + ":";
616         this.target_file_node.nodeValue = info.target_file_text();
617         this.update_time_field();
619         var tran_text = "";
620         if (info.state == DOWNLOAD_FINISHED)
621             tran_text = pretty_print_file_size(info.size).join(" ");
622         else {
623             var trans = pretty_print_file_size(info.amount_transferred);
624             if (info.size >= 0) {
625                 var total = pretty_print_file_size(info.size);
626                 if (trans[1] == total[1])
627                     tran_text += trans[0] + "/" + total[0] + " " + total[1];
628                 else
629                     tran_text += trans.join(" ") + "/" + total.join(" ");
630             } else
631                 tran_text += trans.join(" ");
632         }
633         this.transferred_textnode.nodeValue = tran_text;
634         if (info.percent_complete >= 0) {
635             this.progress_container_node.style.display = "";
636             this.percent_textnode.nodeValue = info.percent_complete + "%";
637             this.progress_bar_node.style.width = info.percent_complete + "%";
638         } else {
639             this.percent_textnode.nodeValue = "";
640             this.progress_container_node.style.display = "none";
641         }
643         this.update_command_field();
644     },
646     update_time_field: function () {
647         var info = this.info;
648         var elapsed_text = pretty_print_time((Date.now() - info.start_time / 1000) / 1000) + " elapsed";
649         var text = "";
650         if (info.state == DOWNLOAD_DOWNLOADING)
651             text = pretty_print_file_size(info.speed).join(" ") + "/s, ";
652         if (info.state == DOWNLOAD_DOWNLOADING &&
653             info.size >= 0 &&
654             info.speed > 0)
655         {
656             let remaining = (info.size - info.amount_transferred) / info.speed;
657             text += pretty_print_time(remaining) + " left (" + elapsed_text + ")";
658         } else
659             text = elapsed_text;
660         this.time_textnode.nodeValue = text;
661     },
663     update_command_field: function () {
664         if (!this.generated)
665             return;
666         if (this.info.shell_command != null) {
667             this.command_div_node.style.display = "";
668             var label;
669             if (this.info.running_shell_command)
670                 label = "Running:";
671             else if (this.info.state == DOWNLOAD_FINISHED)
672                 label = "Ran command:";
673             else
674                 label = "Run command:";
675             this.command_label_textnode.nodeValue = label;
676             this.command_textnode.nodeValue = this.info.shell_command;
677         } else
678             this.command_div_node.style.display = "none";
679     }
682 function download_cancel (buffer) {
683     check_buffer(buffer, download_buffer);
684     var info = buffer.info;
685     info.cancel();
686     buffer.window.minibuffer.message("Download canceled");
688 interactive("download-cancel",
689     "Cancel the current download.\n" +
690     "The download can later be retried using the `download-retry' "+
691     "command, but any data already transferred will be lost.",
692     function (I) {
693         let result = yield I.window.minibuffer.read_single_character_option(
694             $prompt = "Cancel this download? (y/n)",
695             $options = ["y", "n"]);
696         if (result == "y")
697             download_cancel(I.buffer);
698     });
700 function download_retry (buffer) {
701     check_buffer(buffer, download_buffer);
702     var info = buffer.info;
703     info.retry();
704     buffer.window.minibuffer.message("Download retried");
706 interactive("download-retry",
707     "Retry a failed or canceled download.\n" +
708     "This command can be used to retry a download that failed or "+
709     "was canceled using the `download-cancel' command.  The download "+
710     "will begin from the start again.",
711     function (I) { download_retry(I.buffer); });
713 function download_pause (buffer) {
714     check_buffer(buffer, download_buffer);
715     buffer.info.pause();
716     buffer.window.minibuffer.message("Download paused");
718 interactive("download-pause",
719     "Pause the current download.\n" +
720     "The download can later be resumed using the `download-resume' command. "+
721     "The data already transferred will not be lost.",
722     function (I) { download_pause(I.buffer); });
724 function download_resume (buffer) {
725     check_buffer(buffer, download_buffer);
726     buffer.info.resume();
727     buffer.window.minibuffer.message("Download resumed");
729 interactive("download-resume",
730     "Resume the current download.\n" +
731     "This command can be used to resume a download paused using the "+
732     "`download-pause' command.",
733     function (I) { download_resume(I.buffer); });
735 function download_remove (buffer) {
736     check_buffer(buffer, download_buffer);
737     buffer.info.remove();
738     buffer.window.minibuffer.message("Download removed");
740 interactive("download-remove",
741     "Remove the current download from the download manager.\n" +
742     "This command can only be used on inactive (paused, canceled, "+
743     "completed, or failed) downloads.",
744     function (I) { download_remove(I.buffer); });
746 function download_retry_or_resume (buffer) {
747     check_buffer(buffer, download_buffer);
748     var info = buffer.info;
749     if (info.state == DOWNLOAD_PAUSED)
750         download_resume(buffer);
751     else
752         download_retry(buffer);
754 interactive("download-retry-or-resume",
755     "Retry or resume the current download.\n" +
756     "This command can be used to resume a download paused using the " +
757     "`download-pause' command or canceled using the `download-cancel' "+
758     "command.",
759     function (I) { download_retry_or_resume(I.buffer); });
761 function download_pause_or_resume (buffer) {
762     check_buffer(buffer, download_buffer);
763     var info = buffer.info;
764     if (info.state == DOWNLOAD_PAUSED)
765         download_resume(buffer);
766     else
767         download_pause(buffer);
769 interactive("download-pause-or-resume",
770     "Pause or resume the current download.\n" +
771     "This command toggles the paused state of the current download.",
772     function (I) { download_pause_or_resume(I.buffer); });
774 function download_delete_target (buffer) {
775     check_buffer(buffer, download_buffer);
776     var info = buffer.info;
777     info.delete_target();
778     buffer.window.minibuffer.message("Deleted file: " + info.target_file.path);
780 interactive("download-delete-target",
781     "Delete the target file of the current download.\n" +
782     "This command can only be used if the download has finished successfully.",
783     function (I) { download_delete_target(I.buffer); });
785 function download_shell_command (buffer, cwd, cmd) {
786     check_buffer(buffer, download_buffer);
787     var info = buffer.info;
788     if (info.state == DOWNLOAD_FINISHED) {
789         shell_command_with_argument_blind(cmd, info.target_file.path, $cwd = cwd);
790         return;
791     }
792     if (info.state != DOWNLOAD_DOWNLOADING && info.state != DOWNLOAD_PAUSED && info.state != DOWNLOAD_QUEUED)
793         info.throw_state_error();
794     if (cmd == null || cmd.length == 0)
795         info.set_shell_command(null, cwd);
796     else
797         info.set_shell_command(cmd, cwd);
798     buffer.window.minibuffer.message("Queued shell command: " + cmd);
800 interactive("download-shell-command",
801     "Run a shell command on the target file of the current download.\n"+
802     "If the download is still in progress, the shell command will be queued "+
803     "to run when the download finishes.",
804     function (I) {
805         var buffer = check_buffer(I.buffer, download_buffer);
806         var cwd = buffer.info.shell_command_cwd || I.local.cwd;
807         var cmd = yield I.minibuffer.read_shell_command(
808             $cwd = cwd,
809             $initial_value = buffer.info.shell_command ||
810                 external_content_handlers.get(buffer.info.MIME_type));
811         download_shell_command(buffer, cwd, cmd);
812     });
814 function download_manager_ui () {}
815 download_manager_ui.prototype = {
816     constructor: download_manager_ui,
817     QueryInterface: XPCOMUtils.generateQI([Ci.nsIDownloadManagerUI]),
819     getAttention: function () {},
820     show: function () {},
821     visible: false
825 interactive("download-manager-show-builtin-ui",
826     "Show the built-in (Firefox-style) download manager window.",
827     function (I) {
828         Components.classesByID["{7dfdf0d1-aff6-4a34-bad1-d0fe74601642}"]
829             .getService(Ci.nsIDownloadManagerUI)
830             .show(I.window);
831     });
835  * Download-show
836  */ 
838 define_variable("download_temporary_file_open_buffer_delay", 500,
839     "Delay (in milliseconds) before a download buffer is opened for "+
840     "temporary downloads.  If the download completes before this amount "+
841     "of time, no download buffer will be opened.  This variable takes "+
842     "effect only if `open_download_buffer_automatically' is in "+
843     "`download_added_hook', which is the case by default.");
845 define_variable("download_buffer_automatic_open_target", OPEN_NEW_WINDOW,
846     "Target(s) for download buffers created by "+
847     "`open_download_buffer_automatically'.");
849 minibuffer_auto_complete_preferences.download = true;
850 minibuffer.prototype.read_download = function () {
851     keywords(arguments,
852              $prompt = "Download",
853              $completer = all_word_completer(
854                  $completions = function (visitor) {
855                      var dls = download_manager_service.activeDownloads;
856                      while (dls.hasMoreElements()) {
857                          let dl = dls.getNext();
858                          visitor(id_to_download_info[dl.id]);
859                      }
860                  },
861                  $get_string = function (x) x.display_name,
862                  $get_description = function (x) x.source.spec,
863                  $get_value = function (x) x),
864              $auto_complete = "download",
865              $auto_complete_initial = true,
866              $match_required = true);
867     var result = yield this.read(forward_keywords(arguments));
868     yield co_return(result);
871 function download_show (window, target, info) {
872     if (! window)
873         target = OPEN_NEW_WINDOW;
874     create_buffer(window, buffer_creator(download_buffer, $info = info), target);
877 function download_show_new_window (I) {
878     var info = yield I.minibuffer.read_download($prompt = "Show download:");
879     download_show(I.window, OPEN_NEW_WINDOW, info);
882 function download_show_new_buffer (I) {
883     var info = yield I.minibuffer.read_download($prompt = "Show download:");
884     download_show(I.window, OPEN_NEW_BUFFER, info);
887 function download_show_new_buffer_background (I) {
888     var info = yield I.minibuffer.read_download($prompt = "Show download:");
889     download_show(I.window, OPEN_NEW_BUFFER_BACKGROUND, info);
892 function open_download_buffer_automatically (info) {
893     var buf = info.source_buffer;
894     var target = download_buffer_automatic_open_target;
895     if (info.temporary_status == DOWNLOAD_NOT_TEMPORARY ||
896         download_temporary_file_open_buffer_delay == 0)
897     {
898         download_show(buf.window, target, info);
899     } else {
900         var timer = null;
901         function finish () {
902             timer.cancel();
903         }
904         add_hook.call(info, "download_finished_hook", finish);
905         timer = call_after_timeout(function () {
906                 remove_hook.call(info, "download_finished_hook", finish);
907                 download_show(buf.window, target, info);
908             }, download_temporary_file_open_buffer_delay);
909     }
911 add_hook("download_added_hook", open_download_buffer_automatically);
913 interactive("download-show",
914     "Prompt for an ongoing download and open a download buffer showing "+
915     "its progress.",
916     alternates(download_show_new_buffer,
917                download_show_new_window));
919 provide("download-manager");