session_save_hist_index: option to save buffer last-access order in sessions
[conkeror.git] / modules / session.js
blob34b5ff17e1e7bdfb255639017ebb5db4729c414b
1 /**
2  * (C) Copyright 2009 Nicholas A. Zigarovich
3  *
4  * Use, modification, and distribution are subject to the terms specified in the
5  * COPYING file.
6 **/
8 /* TODO
9  *
10  * Features:
11  *
12  * - A session-load-window-replace command which is like
13  *   window-current-replace, but which also recycles existing windows.
14  * - A session-load-window-current-flatten command which loads all buffers in a
15  *   multi-window section in the current window.
16  * - Session files should be more human readable.
17  * - Ability to store arbitrary session data, for example, a buffer's history.
18  * - The previous two features depend on a parser/generator for retroj's
19  *   structured plaintext format.
20  *
21  * Bugs, critical:
22  *
23  * - This module does not work correctly with daemon mode.
24  *
25  * Bugs, deferred:
26  *
27  * - Inhibit loading of the homepage in windows' initial buffers when auto-
28  *   loading a session.
29  * - Auto-save the session when the last conkeror window is closed by a
30  *   window manager event. Currently no session is saved.
31  * - Consider how and which errors should be handled. Too often we silently
32  *   fail and return without telling the user why we are doing so.
33  */
36     //// Manual sessions. ////
39     let _session_dir_default = Cc["@mozilla.org/file/directory_service;1"]
40         .getService(Ci.nsIProperties).get("ProfD", Ci.nsIFile);
41     _session_dir_default.append("sessions");
42     if (! _session_dir_default.exists())
43         _session_dir_default.create(Ci.nsIFile.DIRECTORY_TYPE, 0755);
45     define_variable("session_dir", _session_dir_default,
46         "Default directory for save/load interactive commands.");
48     define_variable("session_save_hist_index", false,
49                     "Whether to store last-accessed order in the sessions file.");
51     /**
52      * session_token is instantiated for the $opener property of buffers
53      * created by the session.  This allows the possibility of knowing,
54      * during buffer init, whether the buffer was created by a session or
55      * by other means.
56      */
57     function session_token () {}
58     session_token.prototype = {
59         constructor: session_token
60     };
62     /**
63      * session_get generates and returns a structure containing session
64      * data for the current group of open windows.
65      */
66     function session_get () {
67         let windows = {};
68         let x = 0;
69         for_each_window(function (w) {
70             let buffers = [];
71             w.buffers.for_each(function (b) {
72                 if (b instanceof content_buffer) {
73                     if (!(session_save_hist_index)) {
74                         buffers.push(b.display_uri_string);
75                     }
76                     else {
77                         buffers.push({url: b.display_uri_string, hist_index: w.buffers.buffer_history.indexOf(b)});
78                     }
79                 }
80             });
81             if (buffers.length == 0)
82                 return;
83             windows[x] = buffers;
84             x++;
85         });
86         return windows;
87     }
89     /**
90      * session_load loads the given session (given as a data structure),
91      * with optional window as the first window to load the session into,
92      * with optional buffer_idx as the buffer index at which to begin
93      * overwriting existing buffers.
94      */
95     function session_load (session, window, buffer_idx) {
96         if (! (session[0] && session[0][0]))
97             throw new Error("Invalid 'session' argument.");
98         let s = 0;
99         var opener = new session_token();
100         if (window) {
101             let bi = buffer_idx != undefined ?
102                 buffer_idx : window.buffers.count;
104             // first kill special buffers slated for recycling.
105             let (b, i = (bi == 0 ? 1 : bi),
106                  safe2kill = bi > 0)
107             {
108                 while ((b = window.buffers.get_buffer(i))) {
109                     if (b instanceof content_buffer) {
110                         safe2kill = true;
111                         ++i;
112                     } else
113                         kill_buffer(b, true);
114                 }
115                 if (bi == 0 &&
116                     (b = window.buffers.get_buffer(0)) &&
117                     !(b instanceof content_buffer))
118                 {
119                     if (! safe2kill)
120                         create_buffer(window,
121                                       buffer_creator(content_buffer,
122                                                      $opener = opener),
123                                       OPEN_NEW_BUFFER_BACKGROUND);
124                     kill_buffer(b, true);
125                 }
126             }
128             // it is now safe to recycle the remaining buffers.
129             for (let i = 0; session[s][i] != undefined; ++i, ++bi) {
130                 let b = window.buffers.get_buffer(bi);
131                 if (b) {
132                     try {
133                         let history = b.web_navigation.sessionHistory;
134                         history.PurgeHistory(history.count);
135                     } catch (e) {}
136                     b.load(session[s][i].url);
137                 } else {
138                     let c = buffer_creator(content_buffer,
139                                            $load = session[s][i].url,
140                                            $opener = opener,
141                                            $position = buffer_position_end);
142                     create_buffer(window, c, OPEN_NEW_BUFFER_BACKGROUND);
143                 }
144             }
145             for (let b = window.buffers.get_buffer(bi); b;
146                  b = window.buffers.get_buffer(bi))
147             {
148                 kill_buffer(b, true);
149             }
150             if ('hist_index' in session[s][0]) {
151                 var ts = session[s].slice(0);
152                 ts.sort(function (b,a) { return a.hist_index - b.hist_index; });
153                 for (let i = 0, m = ts.length; i < m; ++i) {
154                     for (let j = 0, n = window.buffers.count; j < n; j++) {
155                         var b = window.buffers.get_buffer(j);
156                         if (ts[i].url == b.display_uri_string) {
157                             window.buffers.buffer_history.splice(window.buffers.buffer_history.indexOf(b), 1);
158                             window.buffers.buffer_history.unshift(b);
159                             break;
160                         }
161                     }
162                 }
163                 switch_to_buffer(window, window.buffers.buffer_history[0]);
164             }
165             ++s;
166         }
168         function make_init_hook (session) {
169             function init_hook (window) {
170                 for (let i = 1; session[i] != undefined; ++i) {
171                     let c = buffer_creator(content_buffer,
172                                            $load = session[i].url,
173                                            $opener = opener,
174                                            $position = buffer_position_end);
175                     create_buffer(window, c, OPEN_NEW_BUFFER_BACKGROUND);
176                 }
177                 if ('hist_index' in session[0]) {
178                     var ts = session.slice(0);
179                     ts.sort(function (b,a) { return a.hist_index - b.hist_index; });
180                     for (let i = 0, n = ts.length; i < n; ++i) {
181                         for (let j = 0, m = window.buffers.count; j < m; j++) {
182                             var b = window.buffers.get_buffer(j);
183                             if (ts[i].url == b.display_uri_string) {
184                                 window.buffers.buffer_history.splice(window.buffers.buffer_history.indexOf(b), 1);
185                                 window.buffers.buffer_history.unshift(b);
186                                 break;
187                             }
188                         }
189                     }
190                     switch_to_buffer(window, window.buffers.buffer_history[0]);
191                 }
192             }
193             return init_hook;
194         }
196         for (; session[s] != undefined; ++s) {
197             let w = make_window(buffer_creator(content_buffer,
198                                                $load = session[s][0].url,
199                                                $opener = opener));
200             add_hook.call(w, "window_initialize_late_hook",
201                           make_init_hook(session[s]));
202         }
203     }
205     /**
206      * session_load_window_new loads the given session into new windows.
207      */
208     function session_load_window_new (session) {
209         session_load(session);
210     }
212     /**
213      * session_load_window_current loads the given session, with the
214      * session's first window being appended to window.  No existing
215      * buffers will be overwritten.
216      */
217     function session_load_window_current (session, window) {
218         let w = window ? window : get_recent_conkeror_window();
219         session_load(session, w);
220     }
222     /**
223      * session_load_window_current loads the given session, with the
224      * session's first window replacing the given window.  All buffers in
225      * the given window will be overwritten.
226      */
227     function session_load_window_current_replace (session, window) {
228         let w = window ? window : get_recent_conkeror_window();
229         session_load(session, w, 0);
230     }
232     /**
233      * session_write writes the given session to the file given by path.
234      */
235     function session_write (path, session) {
236         if (! (path instanceof Ci.nsIFile))
237             path = make_file(path);
238         if (! session)
239             session = session_get();
240         write_text_file(path, JSON.stringify(session));
241     }
243     /**
244      * session_read reads session data from the given file path,
245      * and returns a decoded session structure.
246      */
247     function session_read (path) {
248         if (! (path instanceof Ci.nsIFile))
249             path = make_file(path);
250         var rv = JSON.parse(read_text_file(path));
251         for (var i in rv) {
252             for (let e = 0; e < rv[i].length; e++) {
253                 if (typeof rv[i][e] == "string") {
254                     rv[i][e] = {url: rv[i][e]};
255                 }
256             }
257         }
258         return rv;
259     }
261     /**
262      * session_remove deletes the given session file.
263      */
264     function session_remove (path) {
265         if (! (path instanceof Ci.nsIFile))
266             path = make_file(path);
267         path.remove(false);
268     }
270     let _session_prompt_file = function (I) {
271         yield co_return(
272             yield I.minibuffer.read_file_path(
273                 $prompt = "Session file:",
274                 $initial_value = session_dir.path,
275                 $history = "save"
276             )
277         );
278     };
280     let _session_file_not_found = function (I, file) {
281         let mb = I ? I.minibuffer : get_recent_conkeror_window().minibuffer;
282         let msg = "Session file not found: " + file.path;
283         mb.message(msg);
284         dumpln(msg);
285     }
287     interactive("session-save",
288         "Save the current session.",
289         function (I) {
290             session_write(make_file(yield _session_prompt_file(I)),
291                           session_get());
292         });
294     interactive("session-load-window-new",
295         "Load a session in a new window.", 
296         function (I) {
297             let file = make_file(yield _session_prompt_file(I));
298             if (! file.exists())
299                 _session_file_not_found(I, file);
300             else
301                 session_load_window_new(session_read(file));
302         });
304     interactive("session-load-window-current",
305         "Load a session in new buffers in the current window.",
306         function (I) {
307             let file = make_file(yield _session_prompt_file(I));
308             if (! file.exists())
309                 _session_file_not_found(I, file);
310             else
311                 session_load_window_current(session_read(file), I.window);
312         });
314     interactive("session-load-window-current-replace", 
315         "Replace all buffers in the current window with buffers "+
316         "in the saved session.",
317         function (I) {
318             let file = make_file(yield _session_prompt_file(I));
319             if (! file.exists())
320                 _session_file_not_found(I, file);
321             else
322                 session_load_window_current_replace(session_read(file),
323                                                     I.window, 0);
324         });
326     interactive("session-remove",
327         "Remove a session file.",
328         function (I) {
329             let file = make_file(yield _session_prompt_file(I));
330             if (! file.exists())
331                 _session_file_not_found(I, file);
332             else
333                 session_remove(file);
334         });
337     //// Auto-save sessions. ////
340     define_variable("session_auto_save_file", "auto-save",
341         "Default filename for the auto-save session.");
343     define_variable("session_auto_save_auto_load", false,
344         'Whether to load the auto-saved session when the browser is started. '+
345         'May be true, false, or "prompt".');
347     function session_auto_save_load_window_new () {
348         session_load_window_new(_session_auto_save_cached);
349     }
351     function session_auto_save_load_window_current (window) {
352         session_load_window_current(_session_auto_save_cached, window);
353     }
355     function session_auto_save_load_window_current_replace (window) {
356         session_load_window_current_replace(_session_auto_save_cached, window);
357     }
358     
359     define_variable("session_auto_save_auto_load_fn",
360         null,
361         "Function to be called to load the auto-saved session at start-up " +
362         "when URLs are given on the command-line. May be " +
363         "session_auto_save_load_window_new, " + 
364         "session_auto_save_load_window_current, or null. If null, the" +
365         "session will not be auto-loaded when URLs are given.");
367     // Supported values:
368     //   undefined - we have not tried to cache the auto-save.
369     //   null      - we have tried to cache the auto-save, but it didn't exist.
370     //   object    - the cached session object for the auto-save.
371     let _session_auto_save_cached = undefined;
373     let _session_auto_save_file_get = function () {
374         if (session_auto_save_file instanceof Ci.nsIFile)
375             return session_auto_save_file;
376         let f = session_dir.clone();
377         f.append(session_auto_save_file);
378         return f;
379     };
381     function session_auto_save_save () {
382         let f = _session_auto_save_file_get();
383         let s = session_get();
384         if (s[0])
385             session_write(f, s);
386         else if (f.exists())
387             f.remove(false);
388     }
390     function session_auto_save_remove () {
391         let f = _session_auto_save_file_get();
392         if (f.exists())
393             f.remove(false);
394     }
396     let _session_auto_save_auto_load = function (user_gave_urls) {
397         if (! session_auto_save_auto_load)
398             return;
399         if (! _session_auto_save_cached) {
400             _session_file_not_found(null, _session_auto_save_file_get());
401             return;
402         }
403         let do_load = false;
404         let window = get_recent_conkeror_window();
405         if (session_auto_save_auto_load == true)
406             do_load = true;
407         else if (session_auto_save_auto_load == "prompt" && !user_gave_urls) {
408             do_load = (yield window.minibuffer.read_single_character_option(
409                 $prompt = "Load auto-saved session? (y/n)",
410                 $options = ["y", "n"]
411             )) == "y";
412         } else
413             throw new Error("Invalid value for session_auto_save_auto_load: " +
414                             session_auto_save_auto_load);
415         if (! do_load)
416             return;
417         if (user_gave_urls) {
418             if (session_auto_save_auto_load_fn)
419                 session_auto_save_auto_load_fn(window);
420         } else
421             session_auto_save_load_window_current_replace(window);
422     };
424     interactive("session-auto-save-load-window-new",
425         "Load the auto-save session in a new window.",
426         function (I) { 
427             if (_session_auto_save_cached == null)
428                 _session_file_not_found(I, _session_auto_save_file_get());
429             else
430                 session_auto_save_load_window_new();
431         });
433     interactive("session-auto-save-load-window-current",
434         "Load the auto-save session in new buffers in the current window.",
435         function (I) {
436             if (_session_auto_save_cached == null)
437                 _session_file_not_found(I, _session_auto_save_file_get());
438             else
439                 session_auto_save_load_window_current(I.window);
440         });
442     interactive("session-auto-save-load-window-current-replace",
443         "Replace all buffers in the current window with buffers in the "+
444         "auto-saved session.",
445         function (I) {
446             if (_session_auto_save_cached == null)
447                 _session_file_not_found(I, _session_auto_save_file_get());
448             else
449                 session_auto_save_load_window_current_replace(I.window);
450         });
452     interactive("session-auto-save-remove",
453                 "Remove the auto-save session",
454                 session_auto_save_remove);
457     //// auto-save-session-mode ////
460     let _session_auto_save_mode_bootstrap = function (b) {
461         remove_hook("window_initialize_late_hook", _session_auto_save_mode_bootstrap);
462         add_hook("create_buffer_hook", session_auto_save_save);
463         add_hook("kill_buffer_hook", session_auto_save_save);
464         add_hook("move_buffer_hook", session_auto_save_save);
465         add_hook("content_buffer_location_change_hook", session_auto_save_save);
466         add_hook("select_buffer_hook", session_auto_save_save);
467         let user_gave_urls = false;
468         for (let i = 0; i < command_line.length; ++i) {
469             if (command_line[i][0] != '-') {
470                 user_gave_urls = true;
471                 break;
472             }
473         }
474         co_call(_session_auto_save_auto_load(user_gave_urls));
475     };
477     let _session_auto_save_mode_enable = function () {
478         if (_session_auto_save_cached == undefined) {
479             let f = _session_auto_save_file_get();
480             _session_auto_save_cached = f.exists() ? session_read(f) : null;
481         }
482         if (conkeror_started) {
483             add_hook("create_buffer_hook", session_auto_save_save);
484             add_hook("kill_buffer_hook", session_auto_save_save);
485             add_hook("move_buffer_hook", session_auto_save_save);
486             add_hook("content_buffer_location_change_hook", session_auto_save_save);
487             add_hook("select_buffer_hook", session_auto_save_save);
488         } else
489             add_hook("window_initialize_late_hook", _session_auto_save_mode_bootstrap);
490     };
492     let _session_auto_save_mode_disable = function () {
493         remove_hook("create_buffer_hook", session_auto_save_save);
494         remove_hook("kill_buffer_hook", session_auto_save_save);
495         remove_hook("move_buffer_hook", session_auto_save_save);
496         remove_hook("content_buffer_location_change_hook", session_auto_save_save);
497         remove_hook("select_buffer_hook", session_auto_save_save);
498         // Just in case.
499         remove_hook("window_initialize_late_hook", _session_auto_save_mode_bootstrap);
500     };
502     define_global_mode("session_auto_save_mode",
503                        _session_auto_save_mode_enable,
504                        _session_auto_save_mode_disable);
506     session_auto_save_mode(true);
509 provide("session");