]> git.donarmstrong.com Git - dactyl.git/blob - common/content/tabs.js
f8584da2c4c3ef08aa3eebb852670c1975df0490
[dactyl.git] / common / content / tabs.js
1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k at Gmail>
4 //
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
7 "use strict";
8
9 /** @scope modules */
10
11 // TODO: many methods do not work with Thunderbird correctly yet
12
13 /**
14  * @instance tabs
15  */
16 var Tabs = Module("tabs", {
17     init: function () {
18         // used for the "gb" and "gB" mappings to remember the last :buffer[!] command
19         this._lastBufferSwitchArgs = "";
20         this._lastBufferSwitchSpecial = true;
21
22         // hide tabs initially to prevent flickering when 'stal' would hide them
23         // on startup
24         if (config.hasTabbrowser)
25             config.tabStrip.collapsed = true;
26
27         this.tabStyle = styles.system.add("tab-strip-hiding", config.styleableChrome,
28                                           (config.tabStrip.id ? "#" + config.tabStrip.id : ".tabbrowser-strip") +
29                                               "{ visibility: collapse; }",
30                                           false, true);
31
32         dactyl.commands["tabs.select"] = function (event) {
33             tabs.select(event.originalTarget.getAttribute("identifier"));
34         };
35
36         this.tabBinding = styles.system.add("tab-binding", "chrome://browser/content/browser.xul", String.replace(<><![CDATA[
37                 xul|tab { -moz-binding: url(chrome://dactyl/content/bindings.xml#tab) !important; }
38             ]]></>, /tab-./g, function (m) util.OS.isMacOSX ? "tab-mac" : m),
39             false, true);
40
41         this.timeout(function () {
42             for (let { linkedBrowser: { contentDocument } } in values(this.allTabs))
43                 if (contentDocument.readyState === "complete")
44                     dactyl.initDocument(contentDocument);
45         });
46     },
47
48     _alternates: Class.memoize(function () [config.tabbrowser.mCurrentTab, null]),
49
50     cleanup: function cleanup() {
51         for (let [i, tab] in Iterator(this.allTabs)) {
52             let node = function node(class_) document.getAnonymousElementByAttribute(tab, "class", class_);
53             for (let elem in values(["dactyl-tab-icon-number", "dactyl-tab-number"].map(node)))
54                 if (elem)
55                     elem.parentNode.parentNode.removeChild(elem.parentNode);
56         }
57     },
58
59     updateTabCount: function () {
60         for (let [i, tab] in Iterator(this.visibleTabs)) {
61             if (dactyl.has("Gecko2")) {
62                 let node = function node(class_) document.getAnonymousElementByAttribute(tab, "class", class_);
63                 if (!node("dactyl-tab-number")) {
64                     let img = node("tab-icon-image");
65                     if (img) {
66                         let nodes = {};
67                         let dom = util.xmlToDom(<xul xmlns:xul={XUL} xmlns:html={XHTML}
68                             ><xul:hbox highlight="tab-number"><xul:label key="icon" align="center" highlight="TabIconNumber" class="dactyl-tab-icon-number"/></xul:hbox
69                             ><xul:hbox highlight="tab-number"><html:div key="label" highlight="TabNumber" class="dactyl-tab-number"/></xul:hbox
70                         ></xul>.*, document, nodes);
71                         img.parentNode.appendChild(dom);
72                         tab.__defineGetter__("dactylOrdinal", function () Number(nodes.icon.value));
73                         tab.__defineSetter__("dactylOrdinal", function (i) nodes.icon.value = nodes.label.textContent = i);
74                     }
75                 }
76             }
77             tab.setAttribute("dactylOrdinal", i + 1);
78             tab.dactylOrdinal = i + 1;
79         }
80         statusline.updateTabCount(true);
81     },
82
83     _onTabSelect: function () {
84         // TODO: is all of that necessary?
85         //       I vote no. --Kris
86         modes.reset();
87         statusline.updateTabCount(true);
88         this.updateSelectionHistory();
89     },
90
91     get allTabs() Array.slice(config.tabbrowser.tabContainer.childNodes),
92
93     /**
94      * @property {Object} The previously accessed tab or null if no tab
95      *     other than the current one has been accessed.
96      */
97     get alternate() this.allTabs.indexOf(this._alternates[1]) > -1 ? this._alternates[1] : null,
98
99     /**
100      * @property {Iterator(Object)} A genenerator that returns all browsers
101      *     in the current window.
102      */
103     get browsers() {
104         let browsers = config.tabbrowser.browsers;
105         for (let i = 0; i < browsers.length; i++)
106             yield [i, browsers[i]];
107     },
108
109     /**
110      * @property {number} The number of tabs in the current window.
111      */
112     get count() config.tabbrowser.mTabs.length,
113
114     /**
115      * @property {Object} The local options store for the current tab.
116      */
117     get options() {
118         let store = this.localStore;
119         if (!("options" in store))
120             store.options = {};
121         return store.options;
122     },
123
124     get visibleTabs() config.tabbrowser.visibleTabs || this.allTabs.filter(function (tab) !tab.hidden),
125
126     /**
127      * Returns the local state store for the tab at the specified *tabIndex*.
128      * If *tabIndex* is not specified then the current tab is used.
129      *
130      * @param {number} tabIndex
131      * @returns {Object}
132      */
133     // FIXME: why not a tab arg? Why this and the property?
134     //      : To the latter question, because this works for any tab, the
135     //        property doesn't. And the property is so oft-used that it's
136     //        convenient. To the former question, because I think this is mainly
137     //        useful for autocommands, and they get index arguments. --Kris
138     getLocalStore: function (tabIndex) {
139         let tab = this.getTab(tabIndex);
140         if (!tab.dactylStore)
141             tab.dactylStore = {};
142         return tab.dactylStore;
143     },
144
145     /**
146      * @property {Object} The local state store for the currently selected
147      *     tab.
148      */
149     get localStore() this.getLocalStore(),
150
151     /**
152      * @property {Object[]} The array of closed tabs for the current
153      *     session.
154      */
155     get closedTabs() services.json.decode(services.sessionStore.getClosedTabData(window)),
156
157     /**
158      * Clones the specified *tab* and append it to the tab list.
159      *
160      * @param {Object} tab The tab to clone.
161      * @param {boolean} activate Whether to select the newly cloned tab.
162      */
163     cloneTab: function (tab, activate) {
164         let newTab = config.tabbrowser.addTab();
165         Tabs.copyTab(newTab, tab);
166
167         if (activate)
168             config.tabbrowser.mTabContainer.selectedItem = newTab;
169
170         return newTab;
171     },
172
173     /**
174      * Detaches the specified *tab* and open it in a new window. If no tab is
175      * specified the currently selected tab is detached.
176      *
177      * @param {Object} tab The tab to detach.
178      */
179     detachTab: function (tab) {
180         if (!tab)
181             tab = config.tabbrowser.mTabContainer.selectedItem;
182
183         services.windowWatcher
184                 .openWindow(window, window.getBrowserURL(), null, "chrome,dialog=no,all", tab);
185     },
186
187     /**
188      * Returns the index of the tab containing *content*.
189      *
190      * @param {Object} content Either a content window or a content
191      *     document.
192      */
193     // FIXME: Only called once...necessary?
194     getContentIndex: function (content) {
195         for (let [i, browser] in this.browsers) {
196             if (browser.contentWindow == content || browser.contentDocument == content)
197                 return i;
198         }
199         return -1;
200     },
201
202     /**
203      * If TabView exists, returns the Panorama window. If the Panorama
204      * is has not yet initialized, this function will not return until
205      * it has.
206      *
207      * @returns {Window}
208      */
209     getGroups: function () {
210         if ("_groups" in this)
211             return this._groups;
212
213         if (window.TabView && TabView._initFrame)
214             TabView._initFrame();
215
216         let iframe = document.getElementById("tab-view");
217         this._groups = iframe ? iframe.contentWindow : null;
218         if (this._groups)
219             util.waitFor(function () this._groups.TabItems, this);
220         return this._groups;
221     },
222
223     /**
224      * Returns the tab at the specified *index* or the currently selected tab
225      * if *index* is not specified. This is a 0-based index.
226      *
227      * @param {number|Node} index The index of the tab required or the tab itself
228      * @returns {Object}
229      */
230     getTab: function (index) {
231         if (index instanceof Node)
232             return index;
233         if (index != null)
234             return config.tabbrowser.mTabs[index];
235         return config.tabbrowser.mCurrentTab;
236     },
237
238     /**
239      * Returns the index of *tab* or the index of the currently selected tab if
240      * *tab* is not specified. This is a 0-based index.
241      *
242      * @param {<xul:tab/>} tab A tab from the current tab list.
243      * @param {boolean} visible Whether to consider only visible tabs.
244      * @returns {number}
245      */
246     index: function (tab, visible) {
247         let tabs = this[visible ? "visibleTabs" : "allTabs"];
248         return tabs.indexOf(tab || config.tabbrowser.mCurrentTab);
249     },
250
251     /**
252      * @param spec can either be:
253      * - an absolute integer
254      * - "" for the current tab
255      * - "+1" for the next tab
256      * - "-3" for the tab, which is 3 positions left of the current
257      * - "$" for the last tab
258      */
259     indexFromSpec: function (spec, wrap) {
260         if (spec instanceof Node)
261             return this.allTabs.indexOf(spec);
262
263         let tabs     = this.visibleTabs;
264         let position = this.index(null, true);
265
266         if (spec == null || spec === "")
267             return position;
268
269         if (typeof spec === "number")
270             position = spec;
271         else if (spec === "$")
272             position = tabs.length - 1;
273         else if (/^[+-]\d+$/.test(spec))
274             position += parseInt(spec, 10);
275         else if (/^\d+$/.test(spec))
276             position = parseInt(spec, 10);
277         else
278             return -1;
279
280         if (position >= tabs.length)
281             position = wrap ? position % tabs.length : tabs.length - 1;
282         else if (position < 0)
283             position = wrap ? (position % tabs.length) + tabs.length : 0;
284
285         return this.allTabs.indexOf(tabs[position]);
286     },
287
288     /**
289      * Removes all tabs from the tab list except the specified *tab*.
290      *
291      * @param {Object} tab The tab to keep.
292      */
293     keepOnly: function (tab) {
294         config.tabbrowser.removeAllTabsBut(tab);
295     },
296
297     /**
298      * Lists all tabs matching *filter*.
299      *
300      * @param {string} filter A filter matching a substring of the tab's
301      *     document title or URL.
302      */
303     list: function (filter) {
304         completion.listCompleter("buffer", filter);
305     },
306
307     /**
308      * Moves a tab to a new position in the tab list.
309      *
310      * @param {Object} tab The tab to move.
311      * @param {string} spec See {@link Tabs.indexFromSpec}.
312      * @param {boolean} wrap Whether an out of bounds *spec* causes the
313      *     destination position to wrap around the start/end of the tab list.
314      */
315     move: function (tab, spec, wrap) {
316         let index = tabs.indexFromSpec(spec, wrap);
317         config.tabbrowser.moveTabTo(tab, index);
318     },
319
320     /**
321      * Removes the specified *tab* from the tab list.
322      *
323      * @param {Object} tab The tab to remove.
324      * @param {number} count How many tabs to remove.
325      * @param {boolean} focusLeftTab Focus the tab to the left of the removed tab.
326      */
327     remove: function (tab, count, focusLeftTab) {
328         count = count || 1;
329         let res = this.count > count;
330
331         let tabs = this.visibleTabs;
332         if (tabs.indexOf(tab) < 0)
333             tabs = this.allTabs;
334         let index = tabs.indexOf(tab);
335
336         let next = index + (focusLeftTab ? -count : count);
337         if (!(next in tabs))
338             next = index + (focusLeftTab ? 1 : -1);
339         if (next in tabs) {
340             this._alternates[0] = tabs[next];
341             config.tabbrowser.mTabContainer.selectedItem = tabs[next];
342         }
343
344         if (focusLeftTab)
345             tabs.slice(Math.max(0, index + 1 - count), index + 1).forEach(config.closure.removeTab);
346         else
347             tabs.slice(index, index + count).forEach(config.closure.removeTab);
348         return res;
349     },
350
351     /**
352      * Reloads the specified tab.
353      *
354      * @param {Object} tab The tab to reload.
355      * @param {boolean} bypassCache Whether to bypass the cache when
356      *     reloading.
357      */
358     reload: function (tab, bypassCache) {
359         try {
360             if (bypassCache) {
361                 const flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
362                 config.tabbrowser.getBrowserForTab(tab).reloadWithFlags(flags);
363             }
364             else
365                 config.tabbrowser.reloadTab(tab);
366         }
367         catch (e if !(e instanceof Error)) {}
368     },
369
370     /**
371      * Reloads all tabs.
372      *
373      * @param {boolean} bypassCache Whether to bypass the cache when
374      *     reloading.
375      */
376     reloadAll: function (bypassCache) {
377         if (bypassCache) {
378             for (let i = 0; i < config.tabbrowser.mTabs.length; i++) {
379                 try {
380                     this.reload(config.tabbrowser.mTabs[i], bypassCache);
381                 }
382                 catch (e) {
383                     // FIXME: can we do anything useful here without stopping the
384                     //        other tabs from reloading?
385                 }
386             }
387         }
388         else
389             config.tabbrowser.reloadAllTabs();
390     },
391
392     /**
393      * Selects the tab at the position specified by *spec*.
394      *
395      * @param {string} spec See {@link Tabs.indexFromSpec}
396      * @param {boolean} wrap Whether an out of bounds *spec* causes the
397      *     selection position to wrap around the start/end of the tab list.
398      */
399     select: function (spec, wrap) {
400         let index = tabs.indexFromSpec(spec, wrap);
401         if (index == -1)
402             dactyl.beep();
403         else
404             config.tabbrowser.mTabContainer.selectedIndex = index;
405     },
406
407     /**
408      * Selects the alternate tab.
409      */
410     selectAlternateTab: function () {
411         dactyl.assert(tabs.alternate != null && tabs.getTab() != tabs.alternate,
412                       _("buffer.noAlternate"));
413         tabs.select(tabs.alternate);
414     },
415
416     /**
417      * Stops loading the specified tab.
418      *
419      * @param {Object} tab The tab to stop loading.
420      */
421     stop: function (tab) {
422         if (config.stop)
423             config.stop(tab);
424         else
425             tab.linkedBrowser.stop();
426     },
427
428     /**
429      * Stops loading all tabs.
430      */
431     stopAll: function () {
432         for (let [, browser] in this.browsers)
433             browser.stop();
434     },
435
436     /**
437      * Selects the tab containing the specified *buffer*.
438      *
439      * @param {string} buffer A string which matches the URL or title of a
440      *     buffer, if it is null, the last used string is used again.
441      * @param {boolean} allowNonUnique Whether to select the first of
442      *     multiple matches.
443      * @param {number} count If there are multiple matches select the
444      *     *count*th match.
445      * @param {boolean} reverse Whether to search the buffer list in
446      *     reverse order.
447      *
448      */
449     // FIXME: help!
450     switchTo: function (buffer, allowNonUnique, count, reverse) {
451         if (buffer != null) {
452             // store this command, so it can be repeated with "B"
453             this._lastBufferSwitchArgs = buffer;
454             this._lastBufferSwitchSpecial = allowNonUnique;
455         }
456         else {
457             buffer = this._lastBufferSwitchArgs;
458             if (allowNonUnique == null) // XXX
459                 allowNonUnique = this._lastBufferSwitchSpecial;
460         }
461
462         if (buffer == "#")
463             return tabs.selectAlternateTab();
464
465         reverse = Boolean(reverse);
466         count = Math.max(1, count || 1) * (1 + -2 * reverse);
467
468         let matches = buffer.match(/^(\d+):?/);
469         if (matches)
470             return tabs.select(this.allTabs[parseInt(matches[1], 10) - 1], false);
471
472         matches = array.nth(tabs.allTabs, function (t) (t.linkedBrowser.lastURI || {}).spec === buffer, 0);
473         if (matches)
474             return tabs.select(matches, false);
475
476         matches = completion.runCompleter("buffer", buffer).map(function (obj) obj.tab);
477
478         if (matches.length == 0)
479             dactyl.echoerr(_("buffer.noMatching", buffer));
480         else if (matches.length > 1 && !allowNonUnique)
481             dactyl.echoerr(_("buffer.multipleMatching", buffer));
482         else {
483             let start = matches.indexOf(tabs.getTab());
484             if (start == -1 && reverse)
485                 start++;
486
487             let index = (start + count) % matches.length;
488             if (index < 0)
489                 index = matches.length + index;
490             tabs.select(matches[index], false);
491         }
492     },
493
494     // NOTE: when restarting a session FF selects the first tab and then the
495     // tab that was selected when the session was created.  As a result the
496     // alternate after a restart is often incorrectly tab 1 when there
497     // shouldn't be one yet.
498     /**
499      * Sets the current and alternate tabs, updating the tab selection
500      * history.
501      *
502      * @param {Array(Object)} tabs The current and alternate tab.
503      * @see tabs#alternate
504      */
505     updateSelectionHistory: function (tabs) {
506         if (!tabs) {
507             if (this.getTab() == this._alternates[0]
508                 || this.alternate && this.allTabs.indexOf(this._alternates[0]) == -1
509                 || this.alternate && config.tabbrowser._removingTabs && config.tabbrowser._removingTabs.indexOf(this._alternates[0]) >= 0)
510                 tabs = [this.getTab(), this.alternate];
511         }
512         this._alternates = tabs || [this.getTab(), this._alternates[0]];
513     }
514 }, {
515     copyTab: function (to, from) {
516         if (!from)
517             from = config.tabbrowser.mTabContainer.selectedItem;
518
519         let tabState = services.sessionStore.getTabState(from);
520         services.sessionStore.setTabState(to, tabState);
521     }
522 }, {
523     load: function () {
524         tabs.updateTabCount();
525     },
526     commands: function () {
527         commands.add(["bd[elete]", "bw[ipeout]", "bun[load]", "tabc[lose]"],
528             "Delete current buffer",
529             function (args) {
530                 let special = args.bang;
531                 let count   = args.count;
532                 let arg     = args[0] || "";
533
534                 if (arg) {
535                     let removed = 0;
536                     let matches = arg.match(/^(\d+):?/);
537
538                     if (matches) {
539                         config.removeTab(tabs.getTab(parseInt(matches[1], 10) - 1));
540                         removed = 1;
541                     }
542                     else {
543                         let str = arg.toLowerCase();
544                         let browsers = config.tabbrowser.browsers;
545
546                         for (let i = browsers.length - 1; i >= 0; i--) {
547                             let host, title, uri = browsers[i].currentURI.spec;
548                             if (browsers[i].currentURI.schemeIs("about")) {
549                                 host = "";
550                                 title = "(Untitled)";
551                             }
552                             else {
553                                 host = browsers[i].currentURI.host;
554                                 title = browsers[i].contentTitle;
555                             }
556
557                             [host, title, uri] = [host, title, uri].map(String.toLowerCase);
558
559                             if (host.indexOf(str) >= 0 || uri == str ||
560                                 (special && (title.indexOf(str) >= 0 || uri.indexOf(str) >= 0))) {
561                                 config.removeTab(tabs.getTab(i));
562                                 removed++;
563                             }
564                         }
565                     }
566
567                     if (removed > 0)
568                         dactyl.echomsg(_("buffer.fewer", removed, removed == 1 ? "" : "s"), 9);
569                     else
570                         dactyl.echoerr(_("buffer.noMatching", arg));
571                 }
572                 else // just remove the current tab
573                     tabs.remove(tabs.getTab(), Math.max(count, 1), special);
574             }, {
575                 argCount: "?",
576                 bang: true,
577                 count: true,
578                 completer: function (context) completion.buffer(context),
579                 literal: 0,
580                 privateData: true
581             });
582
583         commands.add(["keepa[lt]"],
584             "Execute a command without changing the current alternate buffer",
585             function (args) {
586                 let alternate = tabs.alternate;
587
588                 try {
589                     commands.execute(args[0] || "", null, true);
590                 }
591                 finally {
592                     tabs.updateSelectionHistory([tabs.getTab(), alternate]);
593                 }
594             }, {
595                 argCount: "+",
596                 completer: function (context) completion.ex(context),
597                 literal: 0,
598                 subCommand: 0
599             });
600
601         commands.add(["tab"],
602             "Execute a command and tell it to output in a new tab",
603             function (args) {
604                 dactyl.withSavedValues(["forceNewTab"], function () {
605                     this.forceNewTab = true;
606                     commands.execute(args[0] || "", null, true);
607                 });
608             }, {
609                 argCount: "+",
610                 completer: function (context) completion.ex(context),
611                 literal: 0,
612                 subCommand: 0
613             });
614
615         commands.add(["tabd[o]", "bufd[o]"],
616             "Execute a command in each tab",
617             function (args) {
618                 for (let tab in values(tabs.visibleTabs)) {
619                     tabs.select(tab);
620                     dactyl.execute(args[0], null, true);
621                 }
622             }, {
623                 argCount: "1",
624                 completer: function (context) completion.ex(context),
625                 literal: 0,
626                 subCommand: 0
627             });
628
629         commands.add(["tabl[ast]", "bl[ast]"],
630             "Switch to the last tab",
631             function () tabs.select("$", false),
632             { argCount: "0" });
633
634         // TODO: "Zero count" if 0 specified as arg
635         commands.add(["tabp[revious]", "tp[revious]", "tabN[ext]", "tN[ext]", "bp[revious]", "bN[ext]"],
636             "Switch to the previous tab or go [count] tabs back",
637             function (args) {
638                 let count = args.count;
639                 let arg   = args[0];
640
641                 // count is ignored if an arg is specified, as per Vim
642                 if (arg) {
643                     if (/^\d+$/.test(arg))
644                         tabs.select("-" + arg, true);
645                     else
646                         dactyl.echoerr(_("error.trailing"));
647                 }
648                 else if (count > 0)
649                     tabs.select("-" + count, true);
650                 else
651                     tabs.select("-1", true);
652             }, {
653                 argCount: "?",
654                 count: true
655             });
656
657         // TODO: "Zero count" if 0 specified as arg
658         commands.add(["tabn[ext]", "tn[ext]", "bn[ext]"],
659             "Switch to the next or [count]th tab",
660             function (args) {
661                 let count = args.count;
662                 let arg   = args[0];
663
664                 if (arg || count > 0) {
665                     let index;
666
667                     // count is ignored if an arg is specified, as per Vim
668                     if (arg) {
669                         dactyl.assert(/^\d+$/.test(arg), _("error.trailing"));
670                         index = arg - 1;
671                     }
672                     else
673                         index = count - 1;
674
675                     if (index < tabs.count)
676                         tabs.select(index, true);
677                     else
678                         dactyl.beep();
679                 }
680                 else
681                     tabs.select("+1", true);
682             }, {
683                 argCount: "?",
684                 count: true
685             });
686
687         commands.add(["tabr[ewind]", "tabfir[st]", "br[ewind]", "bf[irst]"],
688             "Switch to the first tab",
689             function () { tabs.select(0, false); },
690             { argCount: "0" });
691
692         if (config.hasTabbrowser) {
693             commands.add(["b[uffer]"],
694                 "Switch to a buffer",
695                 function (args) { tabs.switchTo(args[0], args.bang, args.count); }, {
696                     argCount: "?",
697                     bang: true,
698                     count: true,
699                     completer: function (context) completion.buffer(context),
700                     literal: 0,
701                     privateData: true
702                 });
703
704             commands.add(["buffers", "files", "ls", "tabs"],
705                 "Show a list of all buffers",
706                 function (args) { tabs.list(args[0] || ""); }, {
707                     argCount: "?",
708                     literal: 0
709                 });
710
711             commands.add(["quita[ll]", "qa[ll]"],
712                 "Quit " + config.appName,
713                 function (args) { dactyl.quit(false, args.bang); }, {
714                     argCount: "0",
715                     bang: true
716                 });
717
718             commands.add(["reloada[ll]"],
719                 "Reload all tab pages",
720                 function (args) { tabs.reloadAll(args.bang); }, {
721                     argCount: "0",
722                     bang: true
723                 });
724
725             commands.add(["stopa[ll]"],
726                 "Stop loading all tab pages",
727                 function () { tabs.stopAll(); },
728                 { argCount: "0" });
729
730             // TODO: add count support
731             commands.add(["tabm[ove]"],
732                 "Move the current tab after tab N",
733                 function (args) {
734                     let arg = args[0];
735
736                     // FIXME: tabmove! N should probably produce an error
737                     dactyl.assert(!arg || /^([+-]?\d+)$/.test(arg),
738                                   _("error.trailing"));
739
740                     // if not specified, move to after the last tab
741                     tabs.move(config.tabbrowser.mCurrentTab, arg || "$", args.bang);
742                 }, {
743                     argCount: "?",
744                     bang: true
745                 });
746
747             commands.add(["tabo[nly]"],
748                 "Close all other tabs",
749                 function () { tabs.keepOnly(tabs.getTab()); },
750                 { argCount: "0" });
751
752             commands.add(["tabopen", "t[open]", "tabnew"],
753                 "Open one or more URLs in a new tab",
754                 function (args) {
755                     dactyl.open(args[0] || "about:blank", { from: "tabopen", where: dactyl.NEW_TAB, background: args.bang });
756                 }, {
757                     bang: true,
758                     completer: function (context) completion.url(context),
759                     domains: function (args) commands.get("open").domains(args),
760                     literal: 0,
761                     privateData: true
762                 });
763
764             commands.add(["tabde[tach]"],
765                 "Detach current tab to its own window",
766                 function () { tabs.detachTab(null); },
767                 { argCount: "0" });
768
769             commands.add(["tabdu[plicate]"],
770                 "Duplicate current tab",
771                 function (args) {
772                     let tab = tabs.getTab();
773
774                     let activate = args.bang ? true : false;
775                     if (options.get("activate").has("tabopen"))
776                         activate = !activate;
777
778                     for (let i in util.range(0, Math.max(1, args.count)))
779                         tabs.cloneTab(tab, activate);
780                 }, {
781                     argCount: "0",
782                     bang: true,
783                     count: true
784                 });
785
786             // TODO: match window by title too?
787             //     : accept the full :tabmove arg spec for the tab index arg?
788             //     : better name or merge with :tabmove?
789             commands.add(["taba[ttach]"],
790                 "Attach the current tab to another window",
791                 function (args) {
792                     dactyl.assert(args.length <= 2 && !args.some(function (i) !/^\d+$/.test(i)),
793                                   _("error.trailing"));
794
795                     let [winIndex, tabIndex] = args.map(parseInt);
796                     let win = dactyl.windows[winIndex - 1];
797
798                     dactyl.assert(win, _("window.noIndex", winIndex));
799                     dactyl.assert(win != window, _("window.cantAttachSame"));
800
801                     let browser = win.getBrowser();
802                     let dummy = browser.addTab("about:blank");
803                     browser.stop();
804                     // XXX: the implementation of DnD in tabbrowser.xml suggests
805                     // that we may not be guaranteed of having a docshell here
806                     // without this reference?
807                     browser.docShell;
808
809                     let last = browser.mTabs.length - 1;
810
811                     browser.moveTabTo(dummy, Math.constrain(tabIndex || last, 0, last));
812                     browser.selectedTab = dummy; // required
813                     browser.swapBrowsersAndCloseOther(dummy, config.tabbrowser.mCurrentTab);
814                 }, {
815                     argCount: "+",
816                     completer: function (context, args) {
817                         if (args.completeArg == 0) {
818                             context.filters.push(function ({ item }) item != window);
819                             completion.window(context);
820                         }
821                     }
822                 });
823         }
824
825         if (dactyl.has("tabs_undo")) {
826             commands.add(["u[ndo]"],
827                 "Undo closing of a tab",
828                 function (args) {
829                     if (args.length)
830                         args = args[0];
831                     else
832                         args = args.count || 0;
833
834                     let m = /^(\d+)(:|$)/.exec(args || '1');
835                     if (m)
836                         window.undoCloseTab(Number(m[1]) - 1);
837                     else if (args) {
838                         for (let [i, item] in Iterator(tabs.closedTabs))
839                             if (item.state.entries[item.state.index - 1].url == args) {
840                                 window.undoCloseTab(i);
841                                 return;
842                             }
843
844                         dactyl.echoerr(_("buffer.noClosed"));
845                     }
846                 }, {
847                     argCount: "?",
848                     completer: function (context) {
849                         context.anchored = false;
850                         context.compare = CompletionContext.Sort.unsorted;
851                         context.filters = [CompletionContext.Filter.textDescription];
852                         context.keys = { text: function ([i, { state: s }]) (i + 1) + ": " + s.entries[s.index - 1].url, description: "[1].title", icon: "[1].image" };
853                         context.completions = Iterator(tabs.closedTabs);
854                     },
855                     count: true,
856                     literal: 0,
857                     privateData: true
858                 });
859
860             commands.add(["undoa[ll]"],
861                 "Undo closing of all closed tabs",
862                 function (args) {
863                     for (let i in Iterator(tabs.closedTabs))
864                         window.undoCloseTab(0);
865
866                 },
867                 { argCount: "0" });
868
869         }
870
871         if (dactyl.has("session")) {
872             commands.add(["wqa[ll]", "wq", "xa[ll]"],
873                 "Save the session and quit",
874                 function () { dactyl.quit(true); },
875                 { argCount: "0" });
876         }
877     },
878     events: function () {
879         let tabContainer = config.tabbrowser.mTabContainer;
880         function callback() {
881             tabs.timeout(function () { this.updateTabCount(); });
882         }
883         for (let event in values(["TabMove", "TabOpen", "TabClose"]))
884             events.listen(tabContainer, event, callback, false);
885         events.listen(tabContainer, "TabSelect", tabs.closure._onTabSelect, false);
886     },
887     mappings: function () {
888         mappings.add([modes.NORMAL], ["g0", "g^"],
889             "Go to the first tab",
890             function () { tabs.select(0); });
891
892         mappings.add([modes.NORMAL], ["g$"],
893             "Go to the last tab",
894             function () { tabs.select("$"); });
895
896         mappings.add([modes.NORMAL], ["gt"],
897             "Go to the next tab",
898             function ({ count }) {
899                 if (count != null)
900                     tabs.select(count - 1, false);
901                 else
902                     tabs.select("+1", true);
903             },
904             { count: true });
905
906         mappings.add([modes.NORMAL], ["<C-n>", "<C-Tab>", "<C-PageDown>"],
907             "Go to the next tab",
908             function ({ count }) { tabs.select("+" + (count || 1), true); },
909             { count: true });
910
911         mappings.add([modes.NORMAL], ["gT", "<C-p>", "<C-S-Tab>", "<C-PageUp>"],
912            "Go to previous tab",
913             function ({ count }) { tabs.select("-" + (count || 1), true); },
914             { count: true });
915
916         if (config.hasTabbrowser) {
917             mappings.add([modes.NORMAL], ["b"],
918                 "Open a prompt to switch buffers",
919                 function ({ count }) {
920                     if (count != null)
921                         tabs.switchTo(String(count));
922                     else
923                         CommandExMode().open("buffer! ");
924                 },
925                 { count: true });
926
927             mappings.add([modes.NORMAL], ["B"],
928                 "Show buffer list",
929                 function () { tabs.list(false); });
930
931             mappings.add([modes.NORMAL], ["d"],
932                 "Delete current buffer",
933                 function ({ count }) { tabs.remove(tabs.getTab(), count, false); },
934                 { count: true });
935
936             mappings.add([modes.NORMAL], ["D"],
937                 "Delete current buffer, focus tab to the left",
938                 function ({ count }) { tabs.remove(tabs.getTab(), count, true); },
939                 { count: true });
940
941             mappings.add([modes.NORMAL], ["gb"],
942                 "Repeat last :buffer[!] command",
943                 function ({ count }) { tabs.switchTo(null, null, count, false); },
944                 { count: true });
945
946             mappings.add([modes.NORMAL], ["gB"],
947                 "Repeat last :buffer[!] command in reverse direction",
948                 function ({ count }) { tabs.switchTo(null, null, count, true); },
949                 { count: true });
950
951             // TODO: feature dependencies - implies "session"?
952             if (dactyl.has("tabs_undo")) {
953                 mappings.add([modes.NORMAL], ["u"],
954                     "Undo closing of a tab",
955                     function ({ count }) { ex.undo({ "#": count }); },
956                     { count: true });
957             }
958
959             mappings.add([modes.NORMAL], ["<C-^>", "<C-6>"],
960                 "Select the alternate tab or the [count]th tab",
961                 function ({ count }) {
962                     if (count != null)
963                         tabs.switchTo(String(count), false);
964                     else
965                         tabs.selectAlternateTab();
966                 },
967                 { count: true });
968         }
969     },
970     options: function () {
971         options.add(["showtabline", "stal"],
972             "Define when the tab bar is visible",
973             "string", config.defaults["showtabline"],
974             {
975                 setter: function (value) {
976                     if (value === "never")
977                         tabs.tabStyle.enabled = true;
978                     else {
979                         prefs.safeSet("browser.tabs.autoHide", value === "multitab",
980                                       "See 'showtabline' option.");
981                         tabs.tabStyle.enabled = false;
982                     }
983                     if (value !== "multitab" || !dactyl.has("Gecko2"))
984                         config.tabStrip.collapsed = false;
985                     if (config.tabbrowser.tabContainer._positionPinnedTabs)
986                         config.tabbrowser.tabContainer._positionPinnedTabs();
987                     return value;
988                 },
989                 values: {
990                     "never":    "Never show the tab bar",
991                     "multitab": "Show the tab bar when there are multiple tabs",
992                     "always":   "Always show the tab bar"
993                 }
994             });
995
996         if (config.hasTabbrowser) {
997             let activateGroups = [
998                 ["all", "Activate everything"],
999                 ["addons", ":addo[ns] command"],
1000                 ["bookmarks", "Tabs loaded from bookmarks", "loadBookmarksInBackground"],
1001                 ["diverted", "Links with targets set to new tabs", "loadDivertedInBackground"],
1002                 ["downloads", ":downl[oads] command"],
1003                 ["extoptions", ":exto[ptions] command"],
1004                 ["help", ":h[elp] command"],
1005                 ["homepage", "gH mapping"],
1006                 ["links", "Middle- or Control-clicked links", "loadInBackground"],
1007                 ["quickmark", "go and gn mappings"],
1008                 ["tabopen", ":tabopen[!] command"],
1009                 ["paste", "P and gP mappings"]
1010             ];
1011             options.add(["activate", "act"],
1012                 "Define when newly created tabs are automatically activated",
1013                 "stringlist", [g[0] for (g in values(activateGroups.slice(1))) if (!g[2] || !prefs.get("browser.tabs." + g[2]))].join(","),
1014                 {
1015                     values: activateGroups,
1016                     has: Option.has.toggleAll,
1017                     setter: function (newValues) {
1018                         let valueSet = set(newValues);
1019                         for (let group in values(activateGroups))
1020                             if (group[2])
1021                                 prefs.safeSet("browser.tabs." + group[2],
1022                                               !(valueSet["all"] ^ valueSet[group[0]]),
1023                                               "See the 'activate' option");
1024                         return newValues;
1025                     }
1026                 });
1027
1028             options.add(["newtab"],
1029                 "Define which commands should output in a new tab by default",
1030                 "stringlist", "",
1031                 {
1032                     values: {
1033                         "all": "All commands",
1034                         "addons": ":addo[ns] command",
1035                         "downloads": ":downl[oads] command",
1036                         "extoptions": ":exto[ptions] command",
1037                         "help": ":h[elp] command",
1038                         "javascript": ":javascript! or :js! command",
1039                         "prefs": ":pref[erences]! or :prefs! command"
1040                     },
1041                     has: Option.has.toggleAll
1042                 });
1043
1044             // TODO: Is this really applicable to Melodactyl?
1045             options.add(["popups", "pps"],
1046                 "Where to show requested popup windows",
1047                 "stringlist", "tab",
1048                 {
1049                     setter: function (values) {
1050                         let open = 1, restriction = 0;
1051                         for (let [, opt] in Iterator(values)) {
1052                             if (opt == "tab")
1053                                 open = 3;
1054                             else if (opt == "window")
1055                                 open = 2;
1056                             else if (opt == "resized")
1057                                 restriction = 2;
1058                         }
1059
1060                         prefs.safeSet("browser.link.open_newwindow", open,
1061                                       "See 'popups' option.");
1062                         prefs.safeSet("browser.link.open_newwindow.restriction", restriction,
1063                                       "See 'popups' option.");
1064                         return values;
1065                     },
1066                     values: {
1067                         "tab":     "Open popups in a new tab",
1068                         "window":  "Open popups in a new window",
1069                         "resized": "Open resized popups in a new window"
1070                     }
1071                 });
1072         }
1073     }
1074 });
1075
1076 // vim: set fdm=marker sw=4 ts=4 et: