mirror of
https://github.com/vale981/tridactyl
synced 2025-03-04 17:11:40 -05:00
Add tab group commands
Functionality implemented: - :tgroupcreate to create a tab group - :tgroupswitch to switch to or create a tab group - :tgrouplast to switch to the previously active tab group - :tgrouprename to rename the current tab group - :tgroupclose to close all tabs in the current tab group - :tgroupmove to move the current tab to another tab group - :tgroupabort to destroy all tab group information and show all tabs - :tabgroup... aliases for the above commands - Completion for :tgroupswitch and :tgroupmove - Restoring tab group setup on firefox start (seems to just work without any extra code) - Basic mode indicator - Setting for initial urls for to use newly created tab groups (after first) - Setting for whether :tab shows hidden tabs. Edge cases handled - Opening tabs outside of tridactyl mechanisms (e.g. firefox gui, C-t, etc.) - Switching to a hidden tab - Detaching and attaching tabs - Pinned tabs (they can't be hidden; the commands ignore them) - Various other edge cases Fixes #811.
This commit is contained in:
parent
d1eec89c7a
commit
988ec5b770
11 changed files with 752 additions and 7 deletions
|
@ -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`.
|
- 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:
|
- 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.
|
- 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
|
[betas]: https://tridactyl.cmcaine.co.uk/betas/?sort=time&order=desc
|
||||||
|
|
|
@ -122,6 +122,7 @@ export function enableCompletions() {
|
||||||
RssCompletionSource,
|
RssCompletionSource,
|
||||||
SessionsCompletionSource,
|
SessionsCompletionSource,
|
||||||
SettingsCompletionSource,
|
SettingsCompletionSource,
|
||||||
|
TabGroupCompletionSource,
|
||||||
WindowCompletionSource,
|
WindowCompletionSource,
|
||||||
ExtensionsCompletionSource,
|
ExtensionsCompletionSource,
|
||||||
]
|
]
|
||||||
|
@ -392,6 +393,7 @@ Messaging.addListener("commandline_frame", Messaging.attributeCaller(SELF))
|
||||||
|
|
||||||
import { getCommandlineFns } from "@src/lib/commandline_cmds"
|
import { getCommandlineFns } from "@src/lib/commandline_cmds"
|
||||||
import { KeyEventLike } from "./lib/keyseq"
|
import { KeyEventLike } from "./lib/keyseq"
|
||||||
|
import { TabGroupCompletionSource } from "./completions/TabGroup"
|
||||||
commandline_state.fns = getCommandlineFns(commandline_state)
|
commandline_state.fns = getCommandlineFns(commandline_state)
|
||||||
Messaging.addListener(
|
Messaging.addListener(
|
||||||
"commandline_cmd",
|
"commandline_cmd",
|
||||||
|
|
|
@ -168,9 +168,19 @@ export class BufferCompletionSource extends Completions.CompletionSourceFuse {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fillOptions() {
|
private async fillOptions() {
|
||||||
const tabs: browser.tabs.Tab[] = await browserBg.tabs.query({
|
let tabs: browser.tabs.Tab[]
|
||||||
currentWindow: true,
|
|
||||||
|
if (config.get("tabshowhidden") === "true") {
|
||||||
|
tabs = await browserBg.tabs.query({
|
||||||
|
currentWindow: true
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
tabs = await browserBg.tabs.query({
|
||||||
|
currentWindow: true,
|
||||||
|
hidden: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const options = []
|
const options = []
|
||||||
// Get alternative tab, defined as last accessed tab.
|
// Get alternative tab, defined as last accessed tab.
|
||||||
tabs.sort((a, b) => b.lastAccessed - a.lastAccessed)
|
tabs.sort((a, b) => b.lastAccessed - a.lastAccessed)
|
||||||
|
|
70
src/completions/TabGroup.ts
Normal file
70
src/completions/TabGroup.ts
Normal file
|
@ -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`<tr class="TabGroupCompletionOption option">
|
||||||
|
<td class="title">${group}</td>
|
||||||
|
<td class="tabcount">${tabCount} tab${
|
||||||
|
tabCount !== 1 ? "s" : ""
|
||||||
|
}</td>
|
||||||
|
</tr>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -155,6 +155,7 @@ import * as urlutils from "@src/lib/url_util"
|
||||||
import * as scrolling from "@src/content/scrolling"
|
import * as scrolling from "@src/content/scrolling"
|
||||||
import * as R from "ramda"
|
import * as R from "ramda"
|
||||||
import * as visual from "@src/lib/visual"
|
import * as visual from "@src/lib/visual"
|
||||||
|
import { tabTgroup } from "./lib/tab_groups"
|
||||||
/* tslint:disable:import-spacing */
|
/* tslint:disable:import-spacing */
|
||||||
;(window as any).tri = Object.assign(Object.create(null), {
|
;(window as any).tri = Object.assign(Object.create(null), {
|
||||||
browserBg: webext.browserBg,
|
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 mode = newValue
|
||||||
|
let group = ""
|
||||||
let suffix = ""
|
let suffix = ""
|
||||||
let result = ""
|
let result = ""
|
||||||
if (property !== "mode") {
|
if (property !== "mode") {
|
||||||
if (property === "suffix") {
|
if (property === "suffix") {
|
||||||
mode = oldMode
|
mode = oldMode
|
||||||
suffix = newValue
|
suffix = newValue
|
||||||
} else {
|
} else if (property === "group") {
|
||||||
return
|
mode = oldMode
|
||||||
|
group = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -330,10 +333,17 @@ config.getAsync("modeindicator").then(mode => {
|
||||||
} else {
|
} else {
|
||||||
result = mode
|
result = mode
|
||||||
}
|
}
|
||||||
|
|
||||||
const modeindicatorshowkeys = Config.get("modeindicatorshowkeys")
|
const modeindicatorshowkeys = Config.get("modeindicatorshowkeys")
|
||||||
if (modeindicatorshowkeys === "true" && suffix !== "") {
|
if (modeindicatorshowkeys === "true" && suffix !== "") {
|
||||||
result = mode + " " + suffix
|
result = mode + " " + suffix
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tabGroup = await tabTgroup()
|
||||||
|
if (tabGroup) {
|
||||||
|
result = result + " | " + tabGroup
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"statusindicator: ",
|
"statusindicator: ",
|
||||||
result,
|
result,
|
||||||
|
|
|
@ -20,6 +20,8 @@ export class PrevInput {
|
||||||
class ContentState {
|
class ContentState {
|
||||||
mode: ModeName = "normal"
|
mode: ModeName = "normal"
|
||||||
suffix = ""
|
suffix = ""
|
||||||
|
// to trigger status indicator updates
|
||||||
|
group: string = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ContentStateProperty =
|
export type ContentStateProperty =
|
||||||
|
@ -27,6 +29,7 @@ export type ContentStateProperty =
|
||||||
| "cmdHistory"
|
| "cmdHistory"
|
||||||
| "prevInputs"
|
| "prevInputs"
|
||||||
| "suffix"
|
| "suffix"
|
||||||
|
| "group"
|
||||||
|
|
||||||
export type ContentStateChangedCallback = (
|
export type ContentStateChangedCallback = (
|
||||||
property: ContentStateProperty,
|
property: ContentStateProperty,
|
||||||
|
|
204
src/excmds.ts
204
src/excmds.ts
|
@ -164,6 +164,7 @@ import * as Updates from "@src/lib/updates"
|
||||||
import * as Extensions from "@src/lib/extension_info"
|
import * as Extensions from "@src/lib/extension_info"
|
||||||
import * as webrequests from "@src/background/webrequests"
|
import * as webrequests from "@src/background/webrequests"
|
||||||
import * as commandsHelper from "@src/background/commands"
|
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 = {
|
ALL_EXCMDS = {
|
||||||
"": BGSELF,
|
"": 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
|
// {{{ MISC
|
||||||
|
|
||||||
//#background
|
//#background
|
||||||
|
|
|
@ -543,6 +543,13 @@ export class default_config {
|
||||||
o: "open",
|
o: "open",
|
||||||
w: "winopen",
|
w: "winopen",
|
||||||
t: "tabopen",
|
t: "tabopen",
|
||||||
|
tabgroupabort: "tgroupabort",
|
||||||
|
tabgroupclose: "tgroupclose",
|
||||||
|
tabgroupcreate: "tgroupcreate",
|
||||||
|
tabgrouplast: "tgrouplast",
|
||||||
|
tabgroupmove: "tgroupmove",
|
||||||
|
tabgrouprename: "tgrouprename",
|
||||||
|
tabgroupswitch: "tgroupswitch",
|
||||||
tabnew: "tabopen",
|
tabnew: "tabopen",
|
||||||
tabm: "tabmove",
|
tabm: "tabmove",
|
||||||
tabo: "tabonly",
|
tabo: "tabonly",
|
||||||
|
@ -971,6 +978,16 @@ export class default_config {
|
||||||
*/
|
*/
|
||||||
auconcreatecontainer: "true" | "false" = "true"
|
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.
|
* Number of most recent results to ask Firefox for. We display the top 20 or so most frequently visited ones.
|
||||||
*/
|
*/
|
||||||
|
|
413
src/lib/tab_groups.ts
Normal file
413
src/lib/tab_groups.ts
Normal file
|
@ -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<string>(groups as string[])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current window's tab groups.
|
||||||
|
*
|
||||||
|
* @param groups The set of groups.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export async function setTgroups(groups: Set<string>) {
|
||||||
|
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<number[]> {
|
||||||
|
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<browser.tabs.Tab[]> {
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -61,6 +61,21 @@ export async function activeTabId() {
|
||||||
return (await activeTab()).id
|
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() {
|
export async function activeTabContainerId() {
|
||||||
return (await activeTab()).cookieStoreId
|
return (await activeTab()).cookieStoreId
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,6 +90,7 @@
|
||||||
"search",
|
"search",
|
||||||
"sessions",
|
"sessions",
|
||||||
"storage",
|
"storage",
|
||||||
|
"tabHide",
|
||||||
"tabs",
|
"tabs",
|
||||||
"topSites",
|
"topSites",
|
||||||
"management",
|
"management",
|
||||||
|
|
Loading…
Add table
Reference in a new issue