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