diff --git a/src/completions/Tab.ts b/src/completions/Tab.ts
index c866690b..d28c17c8 100644
--- a/src/completions/Tab.ts
+++ b/src/completions/Tab.ts
@@ -181,21 +181,25 @@ export class BufferCompletionSource extends Completions.CompletionSourceFuse {
private async fillOptions() {
let tabs: browser.tabs.Tab[]
+ // Get alternative tab, defined as last accessed tab in any group in
+ // this window.
+ const currentWindowTabs = await browserBg.tabs.query({
+ currentWindow: true,
+ })
+ currentWindowTabs.sort((a, b) => b.lastAccessed - a.lastAccessed)
+ const altTab = currentWindowTabs[1]
+
if (config.get("tabshowhidden") === "true") {
- tabs = await browserBg.tabs.query({
- currentWindow: true
- })
+ tabs = currentWindowTabs
} else {
tabs = await browserBg.tabs.query({
currentWindow: true,
- hidden: false
+ hidden: false,
})
+ tabs.sort((a, b) => b.lastAccessed - a.lastAccessed)
}
const options = []
- // Get alternative tab, defined as last accessed tab.
- tabs.sort((a, b) => b.lastAccessed - a.lastAccessed)
- const alt = tabs[1]
const useMruTabOrder = config.get("tabsort") === "mru"
if (!useMruTabOrder) {
@@ -221,7 +225,7 @@ export class BufferCompletionSource extends Completions.CompletionSourceFuse {
new BufferCompletionOption(
(tab.index + 1).toString(),
tab,
- tab === alt,
+ tab.index === altTab.index,
tab_container,
),
)
diff --git a/src/completions/TabAll.ts b/src/completions/TabAll.ts
index ebeca3b4..fdc3743a 100644
--- a/src/completions/TabAll.ts
+++ b/src/completions/TabAll.ts
@@ -4,6 +4,7 @@ import * as Containers from "@src/lib/containers"
import * as Completions from "@src/completions"
import * as Messaging from "@src/lib/messaging"
import * as config from "@src/lib/config"
+import { tabTgroup } from "@src/lib/tab_groups"
class TabAllCompletionOption
extends Completions.CompletionOptionHTML
@@ -13,15 +14,54 @@ class TabAllCompletionOption
constructor(
public value: string,
tab: browser.tabs.Tab,
+ isAlternative: boolean,
+ isCurrent: boolean,
winindex: number,
container: browser.contextualIdentities.ContextualIdentity,
incognito: boolean,
+ tgroupname: string,
) {
super()
- this.value = `${winindex}.${tab.index + 1}`
+ const valueStr = `${winindex}.${tab.index + 1}`
+ this.value = valueStr
this.fuseKeys.push(this.value, tab.title, tab.url)
this.tab = tab
+ // pre contains max four uppercase characters for tab status.
+ // If statusstylepretty is set to true replace use unicode characters,
+ // but keep plain letters in hidden column for completion.
+ let preplain = ""
+ if (isCurrent) {
+ preplain += "%"
+ } else if (isAlternative) {
+ preplain += "#"
+ this.value = "#"
+ }
+ let pre = preplain
+ if (tab.pinned) preplain += "P"
+ if (tab.audible) preplain += "A"
+ if (tab.mutedInfo.muted) preplain += "M"
+ if (tab.discarded) preplain += "D"
+
+ if (config.get("completions", "Tab", "statusstylepretty") === "true") {
+ if (tab.pinned) pre += "\uD83D\uDCCC"
+ if (tab.audible) pre += "\uD83D\uDD0A"
+ if (tab.mutedInfo.muted) pre += "\uD83D\uDD07"
+ if (tab.discarded) pre += "\u2296"
+ } else {
+ pre = preplain
+ }
+
+ tgroupname = tgroupname === undefined ? "" : tgroupname
+
+ // Push prefix before padding so we don't match on whitespace
+ this.fuseKeys.push(pre)
+ this.fuseKeys.push(preplain)
+ this.fuseKeys.push(tgroupname)
+
+ // Push properties we want to fuzmatch on
+ this.fuseKeys.push(String(tab.index + 1), tab.title, tab.url)
+
// Create HTMLElement
const favIconUrl = tab.favIconUrl
? tab.favIconUrl
@@ -31,14 +71,16 @@ class TabAllCompletionOption
? "incognito"
: ""}"
>
-
|
+ ${pre} |
+ ${preplain} |
|
|
 |
- ${this.value}: ${tab.title} |
+ ${valueStr}: ${tab.title} |
${tab.url}
|
+ ${tgroupname} |
`
}
}
@@ -147,6 +189,12 @@ export class TabAllCompletionSource extends Completions.CompletionSourceFuse {
return a.windowId - b.windowId
})
+ const currentWindowTabs = await browserBg.tabs.query({
+ currentWindow: true,
+ })
+ currentWindowTabs.sort((a, b) => b.lastAccessed - a.lastAccessed)
+ const altTab = currentWindowTabs[1]
+
// Check to see if this is a command that needs to exclude the current
// window
const excludeCurrentWindow = ["tabgrab"].includes(prefix.trim())
@@ -162,14 +210,20 @@ export class TabAllCompletionSource extends Completions.CompletionSourceFuse {
}
// if we are excluding the current window and this tab is in the current window
// then skip it
- if (excludeCurrentWindow && tab.windowId === currentWindow.id) continue
+ if (excludeCurrentWindow && tab.windowId === currentWindow.id)
+ continue
options.push(
new TabAllCompletionOption(
tab.id.toString(),
tab,
+ tab.index === altTab.index &&
+ tab.windowId === altTab.windowId,
+ tab.active &&
+ tab.windowId === currentWindowTabs[0].windowId,
winindex,
await Containers.getFromId(tab.cookieStoreId),
windows[tab.windowId].incognito,
+ await tabTgroup(tab.id),
),
)
}
diff --git a/src/completions/TabGroup.ts b/src/completions/TabGroup.ts
index 659d8e76..fd0085ea 100644
--- a/src/completions/TabGroup.ts
+++ b/src/completions/TabGroup.ts
@@ -1,20 +1,64 @@
import * as Completions from "@src/completions"
-import { tgroups, windowTgroup, tgroupTabs } from "@src/lib/tab_groups"
+import * as config from "@src/lib/config"
+import {
+ tgroups,
+ windowTgroup,
+ windowLastTgroup,
+ tgroupTabs,
+} from "@src/lib/tab_groups"
-class TabGroupCompletionOption extends Completions.CompletionOptionHTML
+class TabGroupCompletionOption
+ extends Completions.CompletionOptionHTML
implements Completions.CompletionOptionFuse {
public fuseKeys = []
- constructor(group: string, tabCount: number) {
+ constructor(
+ group: string,
+ tabCount: number,
+ current: boolean,
+ alternate: boolean,
+ audible: boolean,
+ urls: string[],
+ ) {
super()
this.value = group
+ let preplain = ""
+ if (current) {
+ preplain += "%"
+ }
+ if (alternate) {
+ preplain += "#"
+ }
+ let pre = preplain
+ if (audible) {
+ preplain += "A"
+ }
+ if (config.get("completions", "Tab", "statusstylepretty") === "true") {
+ if (audible) {
+ pre += "\uD83D\uDD0A"
+ }
+ } else {
+ pre = preplain
+ }
+
this.fuseKeys.push(group)
+ this.fuseKeys.push(pre)
+ this.fuseKeys.push(preplain)
+ this.fuseKeys.push(urls)
+
this.html = html`
+ ${pre} |
+ ${preplain} |
${group} |
- ${tabCount} tab${
- tabCount !== 1 ? "s" : ""
- } |
+
+ ${tabCount} tab${tabCount !== 1 ? "s" : ""}
+ |
+ |
`
+ const urlMarkup = urls.map(
+ u => `${u}`,
+ )
+ this.html.lastElementChild.innerHTML = urlMarkup.join(",")
}
}
@@ -23,7 +67,7 @@ export class TabGroupCompletionSource extends Completions.CompletionSourceFuse {
constructor(private _parent: any) {
super(
- ["tgroupswitch", "tgroupmove"],
+ ["tgroupswitch", "tgroupmove", "tgroupclose"],
"TabGroupCompletionSource",
"Tab Groups",
)
@@ -32,7 +76,11 @@ export class TabGroupCompletionSource extends Completions.CompletionSourceFuse {
this._parent.appendChild(this.node)
}
- async filter(exstr: string) {
+ async onInput(exstr) {
+ return this.updateOptions(exstr)
+ }
+
+ private async updateOptions(exstr = "") {
this.lastExstr = exstr
const [prefix] = this.splitOnPrefix(exstr)
@@ -47,22 +95,28 @@ export class TabGroupCompletionSource extends Completions.CompletionSourceFuse {
return
}
- return this.updateDisplay()
- }
-
- private async updateOptions() {
const currentGroup = await windowTgroup()
- const otherGroups = [...(await tgroups())].filter(
- group => group !== currentGroup,
- )
+ const alternateGroup = await windowLastTgroup()
+ const groups = [...(await tgroups())]
this.options = await Promise.all(
- otherGroups.map(async group => {
- const tabCount = (await tgroupTabs(group)).length
- const o = new TabGroupCompletionOption(group, tabCount)
+ groups.map(async group => {
+ const tabs = await tgroupTabs(group)
+ const audible = tabs.some(t => t.audible)
+ tabs.sort((a, b) => b.lastAccessed - a.lastAccessed)
+ const urls = tabs.map(t => t.url)
+ const o = new TabGroupCompletionOption(
+ group,
+ tabs.length,
+ group === currentGroup,
+ group === alternateGroup,
+ audible,
+ urls,
+ )
o.state = "normal"
return o
}),
)
- return this.updateDisplay()
+ this.completion = undefined
+ return this.updateChain()
}
}
diff --git a/src/excmds.ts b/src/excmds.ts
index 831d9984..32d63aaa 100644
--- a/src/excmds.ts
+++ b/src/excmds.ts
@@ -170,7 +170,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"
+import { tgroups, tgroupActivate, setTabTgroup, setWindowTgroup, setTgroups, windowTgroup, windowLastTgroup, tgroupClearOldInfo, tgroupLastTabId, tgroupTabs, clearAllTgroupInfo, tgroupActivateLast, tgroupHandleTabActivated, tgroupHandleTabCreated, tgroupHandleTabAttached, tgroupHandleTabUpdated, tgroupHandleTabRemoved, tgroupHandleTabDetached } from "./lib/tab_groups"
ALL_EXCMDS = {
"": BGSELF,
@@ -3457,7 +3457,7 @@ export async function tgroupcreate(name: string) {
const promises = []
const groups = await tgroups()
- if (groups.has(name)) {
+ if (groups.has(name) || name === "#") {
throw new Error(`Tab group "${name}" already exists`)
}
@@ -3468,7 +3468,7 @@ export async function tgroupcreate(name: string) {
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 => {
+ browser.tabs.query({ currentWindow: true, pinned: false }).then(tabs => {
setTabTgroup(
name,
tabs.map(({ id }) => id),
@@ -3488,15 +3488,23 @@ export async function tgroupcreate(name: string) {
/**
* Switch to a different tab group, hiding all other tabs.
*
+ * "%" denotes the current tab group and "#" denotes the tab group that was
+ * last active. "A" indates a tab group that contains an audible tab. Use
+ * `:set completions.Tab.statusstylepretty true` to display a unicode character
+ * instead.
+ *
* @param name The name of the tab group to switch to.
*
- * If the tab group does not exist, act like tgroupcreate.
+ * If the tab group does not exist, act like [[tgroupcreate]].
*
*/
//#background
export async function tgroupswitch(name: string) {
+ if (name === "#") {
+ return tgrouplast().then(() => name)
+ }
if (name == (await windowTgroup())) {
- throw new Error(`Already on tab group "${name}"`)
+ return
}
const groups = await tgroups()
@@ -3543,22 +3551,39 @@ export async function tgrouprename(name: string) {
}
/**
- * Close the current tab group.
+ * Close all tabs in a tab group and delete the group.
*
- * First switch to the previously active tab group. Do nothing if there is only
- * one tab group.
+ * @param name The name of the tab group to close. If not specified, close the
+ * current tab group and switch to the previously active tab group.
+ *
+ * Do nothing if there is only one tab group - to discard all tab group
+ * information, use [[tgroupabort]].
*
*/
//#background
-export async function tgroupclose() {
+export async function tgroupclose(name?: string) {
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 (name !== undefined && name !== "#" && !groups.has(name)) {
+ throw new Error(`No tab group named "${name}"`)
} else if (groups.size > 1) {
- const closeGroup = await windowTgroup()
- const newTabGroup = await tgroupActivateLast()
+ const currentGroup = await windowTgroup()
+ let closeGroup = currentGroup
+ if (name === "#") {
+ closeGroup = await windowLastTgroup()
+ if (name === undefined) {
+ throw new Error("No alternate tab group")
+ }
+ } else if (name !== undefined) {
+ closeGroup = name
+ }
+ let newTabGroup = currentGroup
+ if (closeGroup === currentGroup) {
+ newTabGroup = await tgroupActivateLast()
+ }
await tgroupTabs(closeGroup).then(tabs => {
browser.tabs.remove(tabs.map(tab => tab.id))
})
@@ -3567,7 +3592,7 @@ export async function tgroupclose() {
}
/**
- * Move the current tab to another tab group.
+ * Move the current tab to another tab group, creating it if it does not exist.
*
* @param name The name of the tab group to move the tab to.
*
@@ -3583,16 +3608,25 @@ export async function tgroupmove(name: string) {
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}"`)
}
+ if (name === "#") {
+ name = await windowLastTgroup()
+ if (name === undefined) {
+ throw new Error("No alternate tab group")
+ }
+ }
+ if (!groups.has(name)) {
+ // Create new tab group if there isn't one with this name
+ groups.add(name)
+ await setTgroups(groups)
+ }
const tabCount = await tgroupTabs(currentGroup).then(tabs => tabs.length)
await setTabTgroup(name)
+ setContentStateGroup(name)
const currentTabId = await activeTabId()
// switch to other group if this is the last tab in the current group
@@ -4054,7 +4088,7 @@ export async function yankimage(url: string): Promise {
A string following the following format: "[0-9]+.[0-9]+" means the first number being the index of the window that should be selected and the second one being the index of the tab within that window. [[taball]] has completions for this format.
- "%" denotes the current tab and "#" denotes the tab that was last accessed in this window. "P", "A", "M" and "D" indicate tab status (i.e. a pinned, audible, muted or discarded tab. Use `:set completions.Tab.statusstylepretty true` to display unicode characters instead. "P","A","M","D" can be used to filter by tab status in either setting.
+ "%" denotes the current tab and "#" denotes the tab that was last accessed in this window. "P", "A", "M" and "D" indicate tab status (i.e. a pinned, audible, muted or discarded tab). Use `:set completions.Tab.statusstylepretty true` to display unicode characters instead. "P","A","M","D" can be used to filter by tab status in either setting.
A non integer string means to search the URL and title for matches, in this window if called from tab, all windows if called from taball. Title matches can contain '*' as a wildcard.
*/
diff --git a/src/lib/tab_groups.ts b/src/lib/tab_groups.ts
index d74f030b..8007c0ea 100644
--- a/src/lib/tab_groups.ts
+++ b/src/lib/tab_groups.ts
@@ -49,10 +49,10 @@ export async function windowTgroup(id?: number) {
if (id === undefined) {
id = await activeWindowId()
}
- return (browserBg.sessions.getWindowValue(
+ return browserBg.sessions.getWindowValue(
id,
"tridactyl-active-tgroup",
- ) as unknown) as string
+ ) as unknown as string
}
/**
@@ -73,6 +73,22 @@ export async function setWindowTgroup(name: string, id?: number) {
)
}
+/*
+ * Return the last active tab group for a window or undefined.
+ *
+ * @param id The id of the window. Use the current window if not specified.
+ *
+ */
+export async function windowLastTgroup(id?: number) {
+ const otherGroupsTabs = await tgroupTabs(await windowTgroup(id), true)
+ if (otherGroupsTabs.length === 0) {
+ return undefined
+ }
+ otherGroupsTabs.sort((a, b) => b.lastAccessed - a.lastAccessed)
+ const lastTabId = otherGroupsTabs[0].id
+ return tabTgroup(lastTabId)
+}
+
/**
* Clear the active tab group for the current window.
*
@@ -91,10 +107,10 @@ export async function tabTgroup(id?: number) {
if (id === undefined) {
id = await activeTabId()
}
- return (browserBg.sessions.getTabValue(
+ return browserBg.sessions.getTabValue(
id,
"tridactyl-tgroup",
- ) as unknown) as string
+ ) as unknown as string
}
/**
@@ -252,12 +268,8 @@ export async function tgroupActivate(name: string) {
*
*/
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))
+ const lastTabGroup = await windowLastTgroup()
+ return tgroupActivate(lastTabGroup).then(() => lastTabGroup)
}
/**
@@ -325,10 +337,14 @@ export async function tgroupHandleTabActivated(activeInfo) {
await setWindowTgroup(tabGroup, activeInfo.windowId)
promises.push(
- tgroupTabs(tabGroup, false, activeInfo.windowId).then(tabs => browserBg.tabs.show(tabs.map(tab => tab.id))),
+ tgroupTabs(tabGroup, false, activeInfo.windowId).then(tabs =>
+ browserBg.tabs.show(tabs.map(tab => tab.id)),
+ ),
)
promises.push(
- tgroupTabs(tabGroup, true, activeInfo.windowId).then(tabs => browserBg.tabs.hide(tabs.map(tab => tab.id))),
+ tgroupTabs(tabGroup, true, activeInfo.windowId).then(tabs =>
+ browserBg.tabs.hide(tabs.map(tab => tab.id)),
+ ),
)
}
return Promise.all(promises)
diff --git a/src/static/css/commandline.css b/src/static/css/commandline.css
index c34cb6cb..fb9bbdad 100644
--- a/src/static/css/commandline.css
+++ b/src/static/css/commandline.css
@@ -79,6 +79,17 @@ input:focus {
#completions table tr td.privatewindow {
width: 1.5em;
}
+#completions table tr td.tabcount {
+ width: 6em;
+}
+#completions table tr td.tgroup {
+ width: 10em;
+ padding-left: 0.5em;
+ text-align: right;
+}
+#completions table tr td.tgroup:empty {
+ display: none;
+}
/* #completions table tr td:nth-of-type(3) { width: 5em; } */
#completions table tr td.content {
width: 50%;
@@ -259,6 +270,14 @@ a.url:hover {
text-overflow: ellipsis;
}
+#completions .TabGroupCompletionOption td.title {
+ width: 15%;
+}
+
+#completions .TabGroupCompletionOption td.content {
+ width: auto;
+}
+
.HelpCompletionOption td.name {
width: 25%;
}