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%; }