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