7318c1932a8749b3efc8140a23a746a99aec4cc5
[conkeror.git] / modules / session.js
blob7318c1932a8749b3efc8140a23a746a99aec4cc5
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({
75                             url: b.display_uri_string,
76                             hist_index: w.buffers.buffer_history.indexOf(b)
77                         });
78                     } else {
79                         buffers.push(b.display_uri_string);
80                     }
81                 }
82             });
83             if (buffers.length == 0)
84                 return;
85             windows[x] = buffers;
86             x++;
87         });
88         return windows;
89     }
91     /**
92      * session_load loads the given session (given as a data structure),
93      * with optional window as the first window to load the session into,
94      * with optional buffer_idx as the buffer index at which to begin
95      * overwriting existing buffers.
96      */
97     function session_load (session, window, buffer_idx) {
98         if (! (session[0] && session[0][0]))
99             throw new Error("Invalid 'session' argument.");
100         let s = 0;
101         var opener = new session_token();
102         if (window) {
103             let bi = buffer_idx != undefined ?
104                 buffer_idx : window.buffers.count;
106             // first kill special buffers slated for recycling.
107             let (b, i = (bi == 0 ? 1 : bi),
108                  safe2kill = bi > 0)
109             {
110                 while ((b = window.buffers.get_buffer(i))) {
111                     if (b instanceof content_buffer) {
112                         safe2kill = true;
113                         ++i;
114                     } else
115                         kill_buffer(b, true);
116                 }
117                 if (bi == 0 &&
118                     (b = window.buffers.get_buffer(0)) &&
119                     !(b instanceof content_buffer))
120                 {
121                     if (! safe2kill)
122                         create_buffer(window,
123                                       buffer_creator(content_buffer,
124                                                      $opener = opener),
125                                       OPEN_NEW_BUFFER_BACKGROUND);
126                     kill_buffer(b, true);
127                 }
128             }
130             // it is now safe to recycle the remaining buffers.
131             for (let i = 0; session[s][i] != undefined; ++i, ++bi) {
132                 let b = window.buffers.get_buffer(bi);
133                 if (b) {
134                     try {
135                         let history = b.web_navigation.sessionHistory;
136                         history.PurgeHistory(history.count);
137                     } catch (e) {}
138                     b.load(session[s][i].url);
139                 } else {
140                     let c = buffer_creator(content_buffer,
141                                            $load = session[s][i].url,
142                                            $opener = opener,
143                                            $position = buffer_position_end);
144                     create_buffer(window, c, OPEN_NEW_BUFFER_BACKGROUND);
145                 }
146             }
147             for (let b = window.buffers.get_buffer(bi); b;
148                  b = window.buffers.get_buffer(bi))
149             {
150                 kill_buffer(b, true);
151             }
152             if ('hist_index' in session[s][0]) {
153                 var ts = session[s].slice(0);
154                 ts.sort(function (a, b) { return b.hist_index - a.hist_index; });
155                 for (let i = 0, m = ts.length; i < m; ++i) {
156                     for (let j = 0, n = window.buffers.count; j < n; j++) {
157                         var b = window.buffers.get_buffer(j);
158                         if (ts[i].url == b.display_uri_string) {
159                             window.buffers.buffer_history.splice(
160                                 window.buffers.buffer_history.indexOf(b), 1);
161                             window.buffers.buffer_history.unshift(b);
162                             break;
163                         }
164                     }
165                 }
166                 switch_to_buffer(window, window.buffers.buffer_history[0]);
167             }
168             ++s;
169         }
171         function make_init_hook (session) {
172             function init_hook (window) {
173                 for (let i = 1; session[i] != undefined; ++i) {
174                     let c = buffer_creator(content_buffer,
175                                            $load = session[i].url,
176                                            $opener = opener,
177                                            $position = buffer_position_end);
178                     create_buffer(window, c, OPEN_NEW_BUFFER_BACKGROUND);
179                 }
180                 if ('hist_index' in session[0]) {
181                     var ts = session.slice(0);
182                     ts.sort(function (a, b) { return b.hist_index - a.hist_index; });
183                     for (let i = 0, n = ts.length; i < n; ++i) {
184                         for (let j = 0, m = window.buffers.count; j < m; j++) {
185                             var b = window.buffers.get_buffer(j);
186                             if (ts[i].url == b.display_uri_string) {
187                                 window.buffers.buffer_history.splice(
188                                     window.buffers.buffer_history.indexOf(b), 1);
189                                 window.buffers.buffer_history.unshift(b);
190                                 break;
191                             }
192                         }
193                     }
194                     switch_to_buffer(window, window.buffers.buffer_history[0]);
195                 }
196             }
197             return init_hook;
198         }
200         for (; session[s] != undefined; ++s) {
201             let w = make_window(buffer_creator(content_buffer,
202                                                $load = session[s][0].url,
203                                                $opener = opener));
204             add_hook.call(w, "window_initialize_late_hook",
205                           make_init_hook(session[s]));
206         }
207     }
209     /**
210      * session_load_window_new loads the given session into new windows.
211      */
212     function session_load_window_new (session) {
213         session_load(session);
214     }
216     /**
217      * session_load_window_current loads the given session, with the
218      * session's first window being appended to window.  No existing
219      * buffers will be overwritten.
220      */
221     function session_load_window_current (session, window) {
222         let w = window ? window : get_recent_conkeror_window();
223         session_load(session, w);
224     }
226     /**
227      * session_load_window_current loads the given session, with the
228      * session's first window replacing the given window.  All buffers in
229      * the given window will be overwritten.
230      */
231     function session_load_window_current_replace (session, window) {
232         let w = window ? window : get_recent_conkeror_window();
233         session_load(session, w, 0);
234     }
236     /**
237      * session_write writes the given session to the file given by path.
238      */
239     function session_write (path, session) {
240         if (! (path instanceof Ci.nsIFile))
241             path = make_file(path);
242         if (! session)
243             session = session_get();
244         write_text_file(path, JSON.stringify(session));
245     }
247     /**
248      * session_read reads session data from the given file path,
249      * and returns a decoded session structure.
250      */
251     function session_read (path) {
252         if (! (path instanceof Ci.nsIFile))
253             path = make_file(path);
254         var rv = JSON.parse(read_text_file(path));
255         for (var i in rv) {
256             for (let e = 0, n = rv[i].length; e < n; ++e) {
257                 if (typeof rv[i][e] == "string")
258                     rv[i][e] = { url: rv[i][e] };
259             }
260         }
261         return rv;
262     }
264     /**
265      * session_remove deletes the given session file.
266      */
267     function session_remove (path) {
268         if (! (path instanceof Ci.nsIFile))
269             path = make_file(path);
270         path.remove(false);
271     }
273     let _session_prompt_file = function (I) {
274         yield co_return(
275             yield I.minibuffer.read_file_path(
276                 $prompt = "Session file:",
277                 $initial_value = session_dir.path,
278                 $history = "save"
279             )
280         );
281     };
283     let _session_file_not_found = function (I, file) {
284         let mb = I ? I.minibuffer : get_recent_conkeror_window().minibuffer;
285         let msg = "Session file not found: " + file.path;
286         mb.message(msg);
287         dumpln(msg);
288     }
290     interactive("session-save",
291         "Save the current session.",
292         function (I) {
293             session_write(make_file(yield _session_prompt_file(I)),
294                           session_get());
295         });
297     interactive("session-load-window-new",
298         "Load a session in a new window.",
299         function (I) {
300             let file = make_file(yield _session_prompt_file(I));
301             if (! file.exists())
302                 _session_file_not_found(I, file);
303             else
304                 session_load_window_new(session_read(file));
305         });
307     interactive("session-load-window-current",
308         "Load a session in new buffers in the current window.",
309         function (I) {
310             let file = make_file(yield _session_prompt_file(I));
311             if (! file.exists())
312                 _session_file_not_found(I, file);
313             else
314                 session_load_window_current(session_read(file), I.window);
315         });
317     interactive("session-load-window-current-replace",
318         "Replace all buffers in the current window with buffers "+
319         "in the saved session.",
320         function (I) {
321             let file = make_file(yield _session_prompt_file(I));
322             if (! file.exists())
323                 _session_file_not_found(I, file);
324             else
325                 session_load_window_current_replace(session_read(file),
326                                                     I.window, 0);
327         });
329     interactive("session-remove",
330         "Remove a session file.",
331         function (I) {
332             let file = make_file(yield _session_prompt_file(I));
333             if (! file.exists())
334                 _session_file_not_found(I, file);
335             else
336                 session_remove(file);
337         });
340     //// Auto-save sessions. ////
343     define_variable("session_auto_save_file", "auto-save",
344         "Default filename for the auto-save session.");
346     define_variable("session_auto_save_auto_load", false,
347         'Whether to load the auto-saved session when the browser is started. '+
348         'May be true, false, or "prompt".');
350     function session_auto_save_load_window_new () {
351         session_load_window_new(_session_auto_save_cached);
352     }
354     function session_auto_save_load_window_current (window) {
355         session_load_window_current(_session_auto_save_cached, window);
356     }
358     function session_auto_save_load_window_current_replace (window) {
359         session_load_window_current_replace(_session_auto_save_cached, window);
360     }
362     define_variable("session_auto_save_auto_load_fn",
363         null,
364         "Function to be called to load the auto-saved session at start-up " +
365         "when URLs are given on the command-line. May be " +
366         "session_auto_save_load_window_new, " +
367         "session_auto_save_load_window_current, or null. If null, the" +
368         "session will not be auto-loaded when URLs are given.");
370     // Supported values:
371     //   undefined - we have not tried to cache the auto-save.
372     //   null      - we have tried to cache the auto-save, but it didn't exist.
373     //   object    - the cached session object for the auto-save.
374     let _session_auto_save_cached = undefined;
376     let _session_auto_save_file_get = function () {
377         if (session_auto_save_file instanceof Ci.nsIFile)
378             return session_auto_save_file;
379         let f = session_dir.clone();
380         f.append(session_auto_save_file);
381         return f;
382     };
384     function session_auto_save_save () {
385         let f = _session_auto_save_file_get();
386         let s = session_get();
387         if (s[0])
388             session_write(f, s);
389         else if (f.exists())
390             f.remove(false);
391     }
393     function session_auto_save_remove () {
394         let f = _session_auto_save_file_get();
395         if (f.exists())
396             f.remove(false);
397     }
399     let _session_auto_save_auto_load = function (user_gave_urls) {
400         if (! session_auto_save_auto_load)
401             return;
402         if (! _session_auto_save_cached) {
403             _session_file_not_found(null, _session_auto_save_file_get());
404             return;
405         }
406         let do_load = false;
407         let window = get_recent_conkeror_window();
408         if (session_auto_save_auto_load == true)
409             do_load = true;
410         else if (session_auto_save_auto_load == "prompt" && !user_gave_urls) {
411             do_load = (yield window.minibuffer.read_single_character_option(
412                 $prompt = "Load auto-saved session? (y/n)",
413                 $options = ["y", "n"]
414             )) == "y";
415         } else
416             throw new Error("Invalid value for session_auto_save_auto_load: " +
417                             session_auto_save_auto_load);
418         if (! do_load)
419             return;
420         if (user_gave_urls) {
421             if (session_auto_save_auto_load_fn)
422                 session_auto_save_auto_load_fn(window);
423         } else
424             session_auto_save_load_window_current_replace(window);
425     };
427     interactive("session-auto-save-load-window-new",
428         "Load the auto-save session in a new window.",
429         function (I) {
430             if (_session_auto_save_cached == null)
431                 _session_file_not_found(I, _session_auto_save_file_get());
432             else
433                 session_auto_save_load_window_new();
434         });
436     interactive("session-auto-save-load-window-current",
437         "Load the auto-save session in new buffers in the current window.",
438         function (I) {
439             if (_session_auto_save_cached == null)
440                 _session_file_not_found(I, _session_auto_save_file_get());
441             else
442                 session_auto_save_load_window_current(I.window);
443         });
445     interactive("session-auto-save-load-window-current-replace",
446         "Replace all buffers in the current window with buffers in the "+
447         "auto-saved session.",
448         function (I) {
449             if (_session_auto_save_cached == null)
450                 _session_file_not_found(I, _session_auto_save_file_get());
451             else
452                 session_auto_save_load_window_current_replace(I.window);
453         });
455     interactive("session-auto-save-remove",
456                 "Remove the auto-save session",
457                 session_auto_save_remove);
460     //// auto-save-session-mode ////
463     let _session_auto_save_mode_bootstrap = function (b) {
464         remove_hook("window_initialize_late_hook", _session_auto_save_mode_bootstrap);
465         add_hook("create_buffer_hook", session_auto_save_save);
466         add_hook("kill_buffer_hook", session_auto_save_save);
467         add_hook("move_buffer_hook", session_auto_save_save);
468         add_hook("content_buffer_location_change_hook", session_auto_save_save);
469         add_hook("select_buffer_hook", session_auto_save_save);
470         let user_gave_urls = false;
471         for (let i = 0; i < command_line.length; ++i) {
472             if (command_line[i][0] != '-') {
473                 user_gave_urls = true;
474                 break;
475             }
476         }
477         co_call(_session_auto_save_auto_load(user_gave_urls));
478     };
480     let _session_auto_save_mode_enable = function () {
481         if (_session_auto_save_cached == undefined) {
482             let f = _session_auto_save_file_get();
483             _session_auto_save_cached = f.exists() ? session_read(f) : null;
484         }
485         if (conkeror_started) {
486             add_hook("create_buffer_hook", session_auto_save_save);
487             add_hook("kill_buffer_hook", session_auto_save_save);
488             add_hook("move_buffer_hook", session_auto_save_save);
489             add_hook("content_buffer_location_change_hook", session_auto_save_save);
490             add_hook("select_buffer_hook", session_auto_save_save);
491         } else
492             add_hook("window_initialize_late_hook", _session_auto_save_mode_bootstrap);
493     };
495     let _session_auto_save_mode_disable = function () {
496         remove_hook("create_buffer_hook", session_auto_save_save);
497         remove_hook("kill_buffer_hook", session_auto_save_save);
498         remove_hook("move_buffer_hook", session_auto_save_save);
499         remove_hook("content_buffer_location_change_hook", session_auto_save_save);
500         remove_hook("select_buffer_hook", session_auto_save_save);
501         // Just in case.
502         remove_hook("window_initialize_late_hook", _session_auto_save_mode_bootstrap);
503     };
505     define_global_mode("session_auto_save_mode",
506                        _session_auto_save_mode_enable,
507                        _session_auto_save_mode_disable);
509     session_auto_save_mode(true);
512 provide("session");