diff --git a/doc/amo.md b/doc/amo.md
index 0e5d0891..fe3ad034 100644
--- a/doc/amo.md
+++ b/doc/amo.md
@@ -59,5 +59,7 @@ Since Tridactyl aims to provide all the features Vimperator and Pentadactyl had,
- This allows us to use Firefox's built-in find-in-page API, for, for example, allowing you to bind find-next and find-previous to `n` and `N`.
- Monitor extension usage and manage themes:
- Tridactyl needs this to integrate with and avoid conflicts with other extensions. For example, Tridactyl's contextual identity features use this to cooperate with the Multi-Account Containers extension.
+- Hide tabs:
+ - Tridactyl needs this for tab group commands, which allow associating names with different groups of tabs and showing the tabs from only of those groups at a time.
[betas]: https://tridactyl.cmcaine.co.uk/betas/?sort=time&order=desc
diff --git a/src/commandline_frame.ts b/src/commandline_frame.ts
index c4d1e85e..2c58bedd 100644
--- a/src/commandline_frame.ts
+++ b/src/commandline_frame.ts
@@ -122,6 +122,7 @@ export function enableCompletions() {
RssCompletionSource,
SessionsCompletionSource,
SettingsCompletionSource,
+ TabGroupCompletionSource,
WindowCompletionSource,
ExtensionsCompletionSource,
]
@@ -392,6 +393,7 @@ Messaging.addListener("commandline_frame", Messaging.attributeCaller(SELF))
import { getCommandlineFns } from "@src/lib/commandline_cmds"
import { KeyEventLike } from "./lib/keyseq"
+import { TabGroupCompletionSource } from "./completions/TabGroup"
commandline_state.fns = getCommandlineFns(commandline_state)
Messaging.addListener(
"commandline_cmd",
diff --git a/src/completions/Tab.ts b/src/completions/Tab.ts
index 3af2f6b4..db4f4c07 100644
--- a/src/completions/Tab.ts
+++ b/src/completions/Tab.ts
@@ -168,9 +168,19 @@ export class BufferCompletionSource extends Completions.CompletionSourceFuse {
}
private async fillOptions() {
- const tabs: browser.tabs.Tab[] = await browserBg.tabs.query({
- currentWindow: true,
- })
+ let tabs: browser.tabs.Tab[]
+
+ if (config.get("tabshowhidden") === "true") {
+ tabs = await browserBg.tabs.query({
+ currentWindow: true
+ })
+ } else {
+ tabs = await browserBg.tabs.query({
+ currentWindow: true,
+ hidden: false
+ })
+ }
+
const options = []
// Get alternative tab, defined as last accessed tab.
tabs.sort((a, b) => b.lastAccessed - a.lastAccessed)
diff --git a/src/completions/TabGroup.ts b/src/completions/TabGroup.ts
new file mode 100644
index 00000000..5ac6cc2f
--- /dev/null
+++ b/src/completions/TabGroup.ts
@@ -0,0 +1,70 @@
+import * as Completions from "@src/completions"
+import { tgroups, windowTgroup, tgroupTabs } from "@src/lib/tab_groups"
+
+class TabGroupCompletionOption extends Completions.CompletionOptionHTML
+ implements Completions.CompletionOptionFuse {
+ public fuseKeys = []
+
+ constructor(group: string, tabCount: number) {
+ super()
+ this.value = group
+ this.fuseKeys.push(group)
+ this.html = html`
+ ${group} |
+ ${tabCount} tab${
+ tabCount !== 1 ? "s" : ""
+ } |
+
`
+ }
+}
+
+export class TabGroupCompletionSource extends Completions.CompletionSourceFuse {
+ public options: TabGroupCompletionOption[]
+
+ constructor(private _parent: any) {
+ super(
+ ["tgroupswitch", "tgroupmove"],
+ "TabGroupCompletionSource",
+ "Tab Groups",
+ )
+
+ this.updateOptions()
+ this._parent.appendChild(this.node)
+ }
+
+ async onInput(_: string) {}
+
+ async filter(exstr: string) {
+ this.lastExstr = exstr
+ const [prefix] = this.splitOnPrefix(exstr)
+
+ // Hide self and stop if prefixes don't match
+ if (prefix) {
+ // Show self if prefix and currently hidden
+ if (this.state === "hidden") {
+ this.state = "normal"
+ }
+ } else {
+ this.state = "hidden"
+ return
+ }
+
+ return this.updateDisplay()
+ }
+
+ private async updateOptions() {
+ const currentGroup = await windowTgroup()
+ const otherGroups = [...(await tgroups())].filter(
+ group => group !== currentGroup,
+ )
+ this.options = await Promise.all(
+ otherGroups.map(async group => {
+ const tabCount = (await tgroupTabs(group)).length
+ const o = new TabGroupCompletionOption(group, tabCount)
+ o.state = "normal"
+ return o
+ }),
+ )
+ return this.updateDisplay()
+ }
+}
diff --git a/src/content.ts b/src/content.ts
index f2419ff2..947e6aa0 100644
--- a/src/content.ts
+++ b/src/content.ts
@@ -155,6 +155,7 @@ import * as urlutils from "@src/lib/url_util"
import * as scrolling from "@src/content/scrolling"
import * as R from "ramda"
import * as visual from "@src/lib/visual"
+import { tabTgroup } from "./lib/tab_groups"
/* tslint:disable:import-spacing */
;(window as any).tri = Object.assign(Object.create(null), {
browserBg: webext.browserBg,
@@ -295,16 +296,18 @@ config.getAsync("modeindicator").then(mode => {
})
}
- addContentStateChangedListener((property, oldMode, oldValue, newValue) => {
+ addContentStateChangedListener(async (property, oldMode, oldValue, newValue) => {
let mode = newValue
+ let group = ""
let suffix = ""
let result = ""
if (property !== "mode") {
if (property === "suffix") {
mode = oldMode
suffix = newValue
- } else {
- return
+ } else if (property === "group") {
+ mode = oldMode
+ group = newValue
}
}
@@ -330,10 +333,17 @@ config.getAsync("modeindicator").then(mode => {
} else {
result = mode
}
+
const modeindicatorshowkeys = Config.get("modeindicatorshowkeys")
if (modeindicatorshowkeys === "true" && suffix !== "") {
result = mode + " " + suffix
}
+
+ const tabGroup = await tabTgroup()
+ if (tabGroup) {
+ result = result + " | " + tabGroup
+ }
+
logger.debug(
"statusindicator: ",
result,
diff --git a/src/content/state_content.ts b/src/content/state_content.ts
index b2a78247..d5948c10 100644
--- a/src/content/state_content.ts
+++ b/src/content/state_content.ts
@@ -20,6 +20,8 @@ export class PrevInput {
class ContentState {
mode: ModeName = "normal"
suffix = ""
+ // to trigger status indicator updates
+ group: string = ""
}
export type ContentStateProperty =
@@ -27,6 +29,7 @@ export type ContentStateProperty =
| "cmdHistory"
| "prevInputs"
| "suffix"
+ | "group"
export type ContentStateChangedCallback = (
property: ContentStateProperty,
diff --git a/src/excmds.ts b/src/excmds.ts
index a8558236..d459ae72 100644
--- a/src/excmds.ts
+++ b/src/excmds.ts
@@ -164,6 +164,7 @@ import * as Updates from "@src/lib/updates"
import * as Extensions from "@src/lib/extension_info"
import * as webrequests from "@src/background/webrequests"
import * as commandsHelper from "@src/background/commands"
+import { tgroups, tgroupActivate, setTabTgroup, setWindowTgroup, setTgroups, windowTgroup, tgroupClearOldInfo, tgroupLastTabId, tgroupTabs, clearAllTgroupInfo, tgroupActivateLast, tgroupHandleTabActivated, tgroupHandleTabCreated, tgroupHandleTabAttached, tgroupHandleTabUpdated, tgroupHandleTabRemoved, tgroupHandleTabDetached } from "./lib/tab_groups"
ALL_EXCMDS = {
"": BGSELF,
@@ -2803,7 +2804,208 @@ export async function recontain(containerName: string) {
}
// }}}
-//
+
+// {{{ TAB GROUPS
+/** @hidden */
+//#background_helper
+// {
+browser.tabs.onCreated.addListener(tgroupHandleTabCreated)
+browser.tabs.onRemoved.addListener(tgroupHandleTabRemoved)
+browser.tabs.onDetached.addListener(tgroupHandleTabDetached)
+browser.tabs.onAttached.addListener(tgroupHandleTabAttached)
+browser.tabs.onActivated.addListener(tgroupHandleTabActivated)
+browser.tabs.onUpdated.addListener(tgroupHandleTabUpdated)
+// }
+
+/** @hidden */
+//#content
+export function setContentStateGroup(name: string) {
+ contentState.group = name
+}
+
+/**
+ * Create a new tab group in the current window.
+ *
+ * Tab groups are a way of organizing different groups of related tabs within a
+ * single window. Groups allow you to have different named contexts and show
+ * only the tabs for a single group at a time.
+ *
+ * @param name The name of the tab group to create.
+ *
+ * If no tab groups exist, set the tab group name for all existing tabs in the
+ * window. Otherwise open a new tab and hide all tabs in the old tab group.
+ *
+ * Tab groups exist only for a single window.
+ *
+ */
+//#background
+export async function tgroupcreate(name: string) {
+ const promises = []
+ const groups = await tgroups()
+
+ if (groups.has(name)) {
+ throw new Error(`Tab group "${name}" already exists`)
+ }
+
+ if (groups.size > 0) {
+ await setWindowTgroup(name)
+ const initialUrl = await config.get("tabgroupnewtaburls")[name]
+ await tabopen(initialUrl)
+ promises.push(tgroupTabs(name, true).then(tabs => browserBg.tabs.hide(tabs.map(tab => tab.id))))
+ } else {
+ promises.push(browser.tabs.query({currentWindow: true}).then((tabs) => {
+ setTabTgroup(name, tabs.map(({ id }) => id))
+ // trigger status line update
+ setContentStateGroup(name)
+ }))
+ promises.push(setWindowTgroup(name))
+ }
+
+ groups.add(name)
+ promises.push(setTgroups(groups))
+ return Promise.all(promises).then(() => name)
+}
+
+/**
+ * Switch to a different tab group, hiding all other tabs.
+ *
+ * @param name The name of the tab group to switch to.
+ *
+ * If the tab group does not exist, act like tgroupcreate.
+ *
+ */
+//#background
+export async function tgroupswitch(name: string) {
+ if (name == await windowTgroup()) {
+ throw new Error(`Already on tab group "${name}"`)
+ }
+
+ const groups = await tgroups()
+ if (groups.size > 0) {
+ if (groups.has(name)) {
+ return tgroupActivate(name).then(() => name)
+ } else {
+ return tgroupcreate(name).then(() => name)
+ }
+ } else {
+ return tgroupcreate(name).then(() => name)
+ }
+}
+
+/**
+ * Switch to the previously active tab group.
+ */
+//#background
+export async function tgrouplast() {
+ if ((await tgroups()).size < 2) {
+ throw new Error("No last tab group")
+ }
+
+ return tgroupActivateLast()
+}
+
+/**
+ * Rename the current tab group.
+ *
+ * @param name The new name of the tab group.
+ *
+ */
+//#background
+export async function tgrouprename(name: string) {
+ if ((await tgroups()).size == 0) {
+ throw new Error("No tab groups exist")
+ }
+
+ return tgroupClearOldInfo(await windowTgroup(), name).then(() => {
+ // trigger status line update
+ setContentStateGroup(name)
+ return name
+ })
+}
+
+/**
+ * Close the current tab group.
+ *
+ * First switch to the previously active tab group. Do nothing if there is only
+ * one tab group.
+ *
+ */
+//#background
+export async function tgroupclose() {
+ const groups = await tgroups()
+ if (groups.size == 0) {
+ throw new Error("No tab groups exist")
+ } else if (groups.size == 1) {
+ throw new Error("This is the only tab group")
+ } else if (groups.size > 1) {
+ const closeGroup = await windowTgroup()
+ const newTabGroup = await tgroupActivateLast()
+ await tgroupTabs(closeGroup).then(tabs => {
+ browser.tabs.remove(tabs.map(tab => tab.id))
+ })
+ return tgroupClearOldInfo(closeGroup).then(() => newTabGroup)
+ }
+}
+
+/**
+ * Move the current tab to another tab group.
+ *
+ * @param name The name of the tab group to move the tab to.
+ *
+ * If this is the last tab in the tab group, also switch to tab group, keeping
+ * the current tab active.
+ *
+ */
+//#background
+export async function tgroupmove(name: string) {
+ const groups = await tgroups()
+ const currentGroup = await windowTgroup()
+
+ if (groups.size == 0) {
+ throw new Error("No tab groups exist")
+ }
+ if (!groups.has(name)) {
+ throw new Error(`Tab group "${name}" does not exist`)
+ }
+ if (name == currentGroup) {
+ throw new Error(`Tab is already on group "${name}"`)
+ }
+
+ const tabCount = await tgroupTabs(currentGroup).then(tabs => tabs.length)
+
+ await setTabTgroup(name)
+ const currentTabId = await activeTabId()
+
+ // switch to other group if this is the last tab in the current group
+ if (tabCount == 1) {
+ return Promise.all([
+ tgroupClearOldInfo(currentGroup, name),
+ tgroupTabs(name).then(tabs => {
+ browserBg.tabs.show(tabs.map(tab => tab.id))
+ }),
+ ]).then(() => name)
+ } else {
+ const lastTabId = await tgroupLastTabId(currentGroup)
+ await tabSetActive(lastTabId)
+ return browser.tabs.hide(currentTabId).then(() => currentGroup)
+ }
+}
+
+/**
+ * Delete all tab group information for the current window and show all tabs.
+ *
+ */
+//#background
+export async function tgroupabort() {
+ if ((await tgroups()).size == 0) {
+ throw new Error("No tab groups exist")
+ }
+
+ return clearAllTgroupInfo().then(() => undefined)
+}
+
+// }}}
+
// {{{ MISC
//#background
diff --git a/src/lib/config.ts b/src/lib/config.ts
index 80ec30e7..9783a3bc 100644
--- a/src/lib/config.ts
+++ b/src/lib/config.ts
@@ -543,6 +543,13 @@ export class default_config {
o: "open",
w: "winopen",
t: "tabopen",
+ tabgroupabort: "tgroupabort",
+ tabgroupclose: "tgroupclose",
+ tabgroupcreate: "tgroupcreate",
+ tabgrouplast: "tgrouplast",
+ tabgroupmove: "tgroupmove",
+ tabgrouprename: "tgrouprename",
+ tabgroupswitch: "tgroupswitch",
tabnew: "tabopen",
tabm: "tabmove",
tabo: "tabonly",
@@ -971,6 +978,16 @@ export class default_config {
*/
auconcreatecontainer: "true" | "false" = "true"
+ /**
+ * Initial urls to navigate to when creating a new tab for a new tab group.
+ */
+ tabgroupnewtaburls = {}
+
+ /**
+ * Whether :tab shows completions for hidden tabs (e.g. tabs in other tab groups).
+ */
+ tabshowhidden: "true" | "false" = "false"
+
/**
* Number of most recent results to ask Firefox for. We display the top 20 or so most frequently visited ones.
*/
diff --git a/src/lib/tab_groups.ts b/src/lib/tab_groups.ts
new file mode 100644
index 00000000..19bdb20a
--- /dev/null
+++ b/src/lib/tab_groups.ts
@@ -0,0 +1,413 @@
+import {
+ activeTabId,
+ activeWindowId,
+ browserBg,
+ removeActiveWindowValue,
+} from "./webext"
+
+/**
+ * Return a set of the current window's tab groups (empty if there are none).
+ *
+ */
+export async function tgroups() {
+ const groups = await browserBg.sessions.getWindowValue(
+ await activeWindowId(),
+ "tridactyl-tgroups",
+ )
+ return new Set(groups as string[])
+}
+
+/**
+ * Set the current window's tab groups.
+ *
+ * @param groups The set of groups.
+ *
+ */
+export async function setTgroups(groups: Set) {
+ return browserBg.sessions.setWindowValue(
+ await activeWindowId(),
+ "tridactyl-tgroups",
+ [...groups],
+ )
+}
+
+/**
+ * Clear the current window's tab groups.
+ *
+ */
+export function clearTgroups() {
+ removeActiveWindowValue("tridactyl-tgroups")
+}
+
+/**
+ * Return the active tab group for the a window or undefined.
+ *
+ * @param id The id of the window. Use the current window if not specified.
+ *
+ */
+export async function windowTgroup(id?: number) {
+ if (id === undefined) {
+ id = await activeWindowId()
+ }
+ return (browserBg.sessions.getWindowValue(
+ id,
+ "tridactyl-active-tgroup",
+ ) as unknown) as string
+}
+
+/**
+ * Set the active tab group for a window.
+ *
+ * @param name The name of the new active tab group.
+ * @param id The id of the window. Use the current window if not specified.
+ *
+ */
+export async function setWindowTgroup(name: string, id?: number) {
+ if (id === undefined) {
+ id = await activeWindowId()
+ }
+ return browserBg.sessions.setWindowValue(
+ id,
+ "tridactyl-active-tgroup",
+ name,
+ )
+}
+
+/**
+ * Clear the active tab group for the current window.
+ *
+ */
+export function clearWindowTgroup() {
+ return removeActiveWindowValue("tridactyl-active-tgroup")
+}
+
+/**
+ * Return a tab's tab group.
+ *
+ * @param id The id of the tab. Use the current tab if not specified.
+ *
+ */
+export async function tabTgroup(id?: number) {
+ if (id === undefined) {
+ id = await activeTabId()
+ }
+ return (browserBg.sessions.getTabValue(
+ id,
+ "tridactyl-tgroup",
+ ) as unknown) as string
+}
+
+/**
+ * Return a list of tab ids.
+ *
+ * @param id An id, a list of ids, or undefined.
+ *
+ * If id is undefined, return a list of the current tab id.
+ *
+ */
+async function tabIdsOrCurrent(ids?: number | number[]): Promise {
+ if (!ids) {
+ ids = [await activeTabId()]
+ } else if (!Array.isArray(ids)) {
+ ids = [ids]
+ }
+ return ids
+}
+
+/**
+ * Set the a tab's tab group.
+ *
+ * @param name The name of the tab group.
+ * @param id The id(s) of the tab(s). Use the current tab if not specified.
+ *
+ */
+export async function setTabTgroup(name: string, id?: number | number[]) {
+ const ids = await tabIdsOrCurrent(id)
+ return Promise.all(
+ ids.map(id => {
+ browserBg.sessions.setTabValue(id, "tridactyl-tgroup", name)
+ }),
+ )
+}
+
+/**
+ * Clear all the tab groups.
+ *
+ * @param id The id(s) of the tab(s). Use the current tab if not specified.
+ *
+ */
+export async function clearTabTgroup(id?: number | number[]) {
+ const ids = await tabIdsOrCurrent(id)
+ return Promise.all(
+ ids.map(id => {
+ browserBg.sessions.removeTabValue(id, "tridactyl-tgroup")
+ }),
+ )
+}
+
+/**
+ * Return a list of all tabs in a tab group.
+ *
+ * @param name The name of the tab group.
+ * @param other Whether to return the tabs not in the tab group instead.
+ * @param id The id of the window. Use the current window if not specified.
+ *
+ */
+export async function tgroupTabs(
+ name: string,
+ other: boolean = false,
+ id?: number,
+): Promise {
+ if (id === undefined) {
+ id = await activeWindowId()
+ }
+ return browserBg.tabs.query({ windowId: id }).then(async tabs => {
+ const sameGroupIndices = await Promise.all(
+ tabs.map(async ({ id }) => {
+ const groupMatched = (await tabTgroup(id)) == name
+ return other ? !groupMatched : groupMatched
+ }),
+ )
+ tabs = tabs.filter((_, index) => sameGroupIndices[index])
+ return tabs
+ })
+}
+
+/**
+ * Return the id of the last selected tab in a tab group.
+ *
+ * @param name The name of the tab group.
+ * @param previous Whether to return the tab selected before the last tab.
+ *
+ */
+export async function tgroupLastTabId(name: string, previous: boolean = false) {
+ const tabs = await tgroupTabs(name)
+ tabs.sort((a, b) => b.lastAccessed - a.lastAccessed)
+ if (previous) {
+ return tabs[1].id
+ } else {
+ return tabs[0].id
+ }
+}
+
+/**
+ * Clear stored information for a tab group.
+ *
+ * @param name The name of the tab group.
+ * @param newName A name to rename the group to.
+ * @param id The id of the window. Use the current window if not specified.
+ *
+ * If newName is specified, add it as a stored group (if it doesn't already
+ * exist), set the current window group to it, and set the group for all tabs in
+ * the old group to it.
+ *
+ */
+export async function tgroupClearOldInfo(
+ oldName: string,
+ newName?: string,
+ id?: number,
+) {
+ const promises = []
+ const groups = await tgroups()
+ groups.delete(oldName)
+ if (newName) {
+ groups.add(newName)
+ }
+ promises.push(setTgroups(groups))
+
+ if (id === undefined) {
+ id = await activeWindowId()
+ }
+
+ if (newName) {
+ promises.push(setWindowTgroup(newName, id))
+ promises.push(
+ tgroupTabs(oldName, false, id).then(tabs => {
+ setTabTgroup(
+ newName,
+ tabs.map(tab => tab.id),
+ )
+ }),
+ )
+ }
+ return Promise.all(promises)
+}
+
+/**
+ * Activate the previously active tab in a tab group.
+ *
+ * @param name The name of the tab group to switch to.
+ *
+ */
+export async function tgroupActivate(name: string) {
+ const lastActiveTabId = await tgroupLastTabId(name)
+ // this will trigger tgroupHandleTabActivated
+ return browserBg.tabs.update(lastActiveTabId, { active: true })
+}
+
+/**
+ * Activate the last active tab in the last active tab group.
+ *
+ * Return the name of activated tab group.
+ *
+ */
+export async function tgroupActivateLast() {
+ const otherGroupsTabs = await tgroupTabs(await windowTgroup(), true)
+ otherGroupsTabs.sort((a, b) => b.lastAccessed - a.lastAccessed)
+ const lastTabId = otherGroupsTabs[0].id
+ return browserBg.tabs
+ .update(lastTabId, { active: true })
+ .then(() => tabTgroup(lastTabId))
+}
+
+/**
+ * Clear all stored tab group information.
+ *
+ */
+export async function clearAllTgroupInfo() {
+ return Promise.all([
+ clearTgroups(),
+ clearWindowTgroup(),
+ browser.tabs.query({ currentWindow: true }).then(async tabs => {
+ const ids = tabs.map(tab => tab.id)
+ await browser.tabs.show(ids)
+ return clearTabTgroup(ids)
+ }),
+ ])
+}
+
+/**
+ * Set the tab's tab group to its window's active tab group if there is one.
+ *
+ * Do nothing if the tab is already associated with a tab group.
+ *
+ * @param tab The tab that was just created.
+ *
+ */
+export async function tgroupHandleTabCreated(tab: browser.tabs.Tab) {
+ const windowGroup = await windowTgroup(tab.windowId)
+
+ if (windowGroup) {
+ const tabGroup = await tabTgroup(tab.id)
+ if (!tabGroup) {
+ return setTabTgroup(windowGroup, tab.id)
+ }
+ }
+}
+
+/**
+ * Set the tab's tab group to its window's active tab group if there is one.
+ *
+ * @param tabId The id of the tab that was just attached to this window.
+ *
+ */
+export async function tgroupHandleTabAttached(tabId: number, attachInfo) {
+ // NOTE this doesn't need to worry about a tab on another window previously
+ // being pinned because tabs become unpinned when you move them between
+ // windows
+ const windowGroup = await windowTgroup(attachInfo.newWindowId)
+ if (windowGroup) {
+ return setTabTgroup(windowGroup, tabId)
+ }
+}
+
+/**
+ * Handle tab activation, possibly switching tab groups.
+ *
+ * If the new tab is from a different tab group, set it as the new tab group,
+ * show all its tabs, and hide all tabs from the old tab group.
+ */
+export async function tgroupHandleTabActivated(activeInfo) {
+ const windowGroup = await windowTgroup(activeInfo.windowId)
+ const tabGroup = await tabTgroup(activeInfo.tab)
+ const promises = []
+ if (windowGroup && tabGroup && windowGroup != tabGroup) {
+ await setWindowTgroup(tabGroup, activeInfo.windowId)
+
+ promises.push(
+ tgroupTabs(tabGroup, false, activeInfo.windowId).then(tabs => {
+ return browserBg.tabs.show(tabs.map(tab => tab.id))
+ }),
+ )
+ promises.push(
+ tgroupTabs(tabGroup, true, activeInfo.windowId).then(tabs => {
+ return browserBg.tabs.hide(tabs.map(tab => tab.id))
+ }),
+ )
+ }
+ return Promise.all(promises)
+}
+
+/**
+ * Set or clear a tab's group if it was pinned or unpinned respectively.
+ *
+ * @param tabId The id of the tab that was just updated.
+ *
+ */
+export async function tgroupHandleTabUpdated(
+ tabId: number,
+ changeInfo,
+ tab: browser.tabs.Tab,
+) {
+ if (changeInfo.pinned !== undefined) {
+ const windowGroup = await windowTgroup(tab.windowId)
+ if (windowGroup) {
+ if (changeInfo.pinned) {
+ return clearTabTgroup(tabId)
+ } else {
+ return setTabTgroup(windowGroup, tabId)
+ }
+ }
+ }
+}
+
+/**
+ * Handle the last tab in a tab group being closed.
+ *
+ * Clear its information.
+ *
+ */
+export async function tgroupHandleTabRemoved(_tabId: number, removeInfo) {
+ if (!removeInfo.isWindowClosing) {
+ const windowGroup = await windowTgroup(removeInfo.windowId)
+ const tabCount = await tgroupTabs(
+ windowGroup,
+ false,
+ removeInfo.windowId,
+ ).then(tabs => tabs.length)
+ if (tabCount == 0) {
+ return tgroupClearOldInfo(
+ windowGroup,
+ undefined,
+ removeInfo.windowId,
+ )
+ }
+ }
+}
+
+/**
+ * Handle the last tab in a tab group being moved to another window.
+ *
+ * Clear its information.
+ *
+ */
+export async function tgroupHandleTabDetached(tabId: number, detachInfo) {
+ // clear tab's stored group; will automatically be changed if there are
+ // groups on the other window but otherwise it will still show up in the
+ // mode indicator
+ clearTabTgroup(tabId)
+
+ const windowGroup = await windowTgroup(detachInfo.oldWindowId)
+ const tabCount = await tgroupTabs(
+ windowGroup,
+ false,
+ detachInfo.oldWindowId,
+ ).then(tabs => tabs.length)
+ if (tabCount == 0) {
+ return tgroupClearOldInfo(
+ windowGroup,
+ undefined,
+ detachInfo.oldWindowId,
+ )
+ }
+}
diff --git a/src/lib/webext.ts b/src/lib/webext.ts
index e16529f9..140ce169 100644
--- a/src/lib/webext.ts
+++ b/src/lib/webext.ts
@@ -61,6 +61,21 @@ export async function activeTabId() {
return (await activeTab()).id
}
+/**
+ * Return the active window's id.
+ *
+ */
+export async function activeWindowId() {
+ return (await browserBg.windows.getCurrent()).id
+}
+
+export async function removeActiveWindowValue(value) {
+ browserBg.sessions.removeWindowValue(
+ await activeWindowId(),
+ value,
+ )
+}
+
export async function activeTabContainerId() {
return (await activeTab()).cookieStoreId
}
diff --git a/src/manifest.json b/src/manifest.json
index 2fbea26b..5eebaec8 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -90,6 +90,7 @@
"search",
"sessions",
"storage",
+ "tabHide",
"tabs",
"topSites",
"management",