-
-
Notifications
You must be signed in to change notification settings - Fork 18
/
Copy pathtabMarkers.ts
201 lines (172 loc) · 5.81 KB
/
tabMarkers.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
import browser from "webextension-polyfill";
import { letterLabels } from "../../common/labels";
import { store } from "../../common/storage/store";
import { type TabMarkers } from "../../typings/TabMarkers";
import { sendMessage } from "../messaging/sendMessage";
import { UnreachableContentScriptError } from "../messaging/UnreachableContentScriptError";
export async function getTabMarker(tabId: number) {
const { assigned } = await store.waitFor("tabMarkers");
return assigned[tabId];
}
export async function getTabIdForMarker(marker: string) {
const { assigned } = await store.waitFor("tabMarkers");
for (const tabId in assigned) {
if (assigned[tabId] === marker) {
return Number(tabId);
}
}
throw new Error(`No tab with the marker "${marker}"`);
}
/**
* Initializes and reconciles the tab markers. It will assign tab markers to the
* tabs that already have one in their title in case the user has the setting
* "Continue where you left off" enabled.
*/
export async function initializeAndReconcileTabMarkers() {
const tabs = await browser.tabs.query({});
const tabsAndTheirMarkers = tabs
.filter((tab) => isTabWithId(tab))
.map((tab) => ({ tab, marker: getMarkerFromTitle(tab.title!) }));
const tabsWithMarkers = tabsAndTheirMarkers.filter((tab) => tab.marker);
const tabsWithoutMarkers = tabsAndTheirMarkers.filter((tab) => !tab.marker);
const tabMarkers = createTabMarkers();
// First assign markers to tabs that already have them
for (const { tab, marker } of tabsWithMarkers) {
assignMarkerToTab(tabMarkers, tab.id, marker);
}
// Then assign new markers to tabs that don't have them
for (const { tab } of tabsWithoutMarkers) {
assignMarkerToTab(tabMarkers, tab.id);
}
await store.set("tabMarkers", tabMarkers);
}
/**
* Adds listeners to the tab cycle events to update the tab markers.
*/
export function addTabMarkerListeners() {
browser.tabs.onCreated.addListener(async ({ id }) => {
if (id) await setTabMarker(id);
});
browser.tabs.onRemoved.addListener(async (tabId) => {
await releaseTabMarker(tabId);
});
// In Chrome when a tab is discarded it changes its id
browser.tabs.onReplaced.addListener(async (addedTabId, removedTabId) => {
const marker = await releaseTabMarker(removedTabId);
await setTabMarker(addedTabId, marker);
});
}
export async function refreshTabMarkers() {
const tabs = await browser.tabs.query({});
const tabWithIds = tabs.filter((tab) => isTabWithId(tab));
// We remove the value here to make sure the initializer is called and using
// `store.waitFor` will wait for the value to be set after the markers have
// been reassigned
await store.remove("tabMarkers");
await store.withLock(
"tabMarkers",
async (tabMarkers) => {
const { free, assigned } = tabMarkers;
for (const tab of tabWithIds) {
const marker = free.pop();
if (marker) assigned[tab.id] = marker;
}
return [tabMarkers];
},
createTabMarkers
);
const refreshing = tabWithIds.map(async (tab) => {
try {
await sendMessage("refreshTitleDecorations", undefined, {
tabId: tab.id,
});
} catch (error: unknown) {
if (!(error instanceof UnreachableContentScriptError)) {
// We simply log the error. We don't throw because we don't want the
// whole command to fail if one tab fails
console.error(error);
}
// We reload if the tab has been discarded and the content script isn't
// running any more. I could check the `discarded` property of the tab but
// I think I did that before and for whatever reason it didn't handle all
// cases. This will make that we also reload any tabs where the content
// script can't run. I think that's ok.
await browser.tabs.reload(tab.id);
}
});
await Promise.all(refreshing);
}
/**
* Sets the tab marker for the given tab id.
*
* @param tabId - The tab id to set the marker for.
* @param preferredMarker - The preferred marker to use.
* @returns The marker that was set.
*/
async function setTabMarker(tabId: number, preferredMarker?: string) {
return store.withLock("tabMarkers", async (tabMarkers) => {
const marker = assignMarkerToTab(tabMarkers, tabId, preferredMarker);
return [tabMarkers, marker];
});
}
/**
* Releases the tab marker for the given tab id.
*
* @param tabId - The tab id to release the marker for.
* @returns The released marker or undefined if the tab doesn't have a marker.
*/
async function releaseTabMarker(tabId: number) {
return store.withLock(
"tabMarkers",
async (tabMarkers) => {
const { free, assigned } = tabMarkers;
const marker = assigned[tabId];
if (!marker) return [tabMarkers];
delete assigned[tabId];
free.push(marker);
free.sort((a, b) => b.length - a.length || b.localeCompare(a));
return [tabMarkers, marker];
},
createTabMarkers
);
}
/**
* Assigns a marker to a tab, modifying the TabMarkers object in place.
*
* @param tabMarkers - The TabMarkers object to modify
* @param tabId - The tab id to set the marker for
* @param preferredMarker - Optional preferred marker to use
* @returns The marker that was assigned, if any
*/
function assignMarkerToTab(
tabMarkers: TabMarkers,
tabId: number,
preferredMarker?: string
) {
if (preferredMarker && tabMarkers.free.includes(preferredMarker)) {
const markerIndex = tabMarkers.free.indexOf(preferredMarker);
tabMarkers.free.splice(markerIndex, 1);
tabMarkers.assigned[tabId] = preferredMarker;
return preferredMarker;
}
const newMarker = tabMarkers.free.pop();
if (newMarker) {
tabMarkers.assigned[tabId] = newMarker;
return newMarker;
}
return undefined;
}
function createTabMarkers(): TabMarkers {
return {
free: [...letterLabels],
assigned: {},
};
}
function getMarkerFromTitle(title: string) {
return /^([a-z]{1,2}) \| /i.exec(title)?.[1]?.toLowerCase();
}
function isTabWithId(
tab: browser.Tabs.Tab
): tab is browser.Tabs.Tab & { id: number } {
return tab.id !== undefined;
}