Skip to content

Commit 528b8ec

Browse files
also search tab.url, added options: "Tab Property Prefixes" and "Hide Header" (#2)
1 parent 1cd3859 commit 528b8ec

File tree

4 files changed

+108
-22
lines changed

4 files changed

+108
-22
lines changed

README.md

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11

22
# TST Tab Search -- filter Tree Style Tab's sidebar
33

4-
This is an extension for the browser extension [Tree Style Tabs](https://github.com/piroor/treestyletab#readme) (TST). It adds a search box at the bottom of TST's sidebar, allowing to search the titles of the tabs in the current window/sidebar, optionally case sensitive, as whole word, or by regular expression.
5-
Matching tabs will be highlighted in the tree, and/or non-matches will be suppressed.
4+
This is an extension for the browser extension [Tree Style Tabs](https://github.com/piroor/treestyletab#readme) (TST). It adds a search box at the bottom of TST's sidebar, allowing to search the titles and URLs (or whatever) of the tabs in the current window/sidebar, optionally case sensitive, as whole word, or by regular expression.
5+
Matching tabs will be highlighted in the tree, and/or non-matches will be suppressed (see extension preferences).
6+
Should the search bar not show up after installing this extension, then have a look at `about:addons` > "Extensions" > "TST Tab Search".
67

78
![Searching for Cats](./resources/screenshot.png)
89

9-
Thats pretty much all there is to say. Might add a few more highlight options.
10-
Many thanks to TST's author [piroor](https://github.com/piroor), who has not only developed TST as a great standalone extension, but also designed a very good API for other extensions to interact and integrate with TST. With that, writing the initial version of this extension from scratch took only about to long afternoons.
10+
Thats pretty much all there is to say.
11+
Many thanks to TST's author [piroor](https://github.com/piroor), who has not only developed TST as a great standalone extension, but also designed a very good API for other extensions to interact and integrate with TST. With that, writing the initial version of this extension from scratch took only about two long afternoons.
1112

1213
<b>Permissions used</b>:
1314

15+
- "Access to browser tabs": Get titles of tabs to be searched.
1416
- "Display notifications to you": Tell you when something goes wrong, (so you should never see this ;) ).
15-
- "Access to browser tabs" (manually granted via TST): Search titles of tabs.
1617

1718
<!-- NOTE: AMO keeps line breaks within paragraphs ... -->
1819

src/background/index.js

+44-13
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ async function register() {
2727
name: manifest.name,
2828
icons: manifest.icons,
2929
listeningTypes: [ 'wait-for-shutdown', ],
30-
style: [ 'hit', 'child', 'miss', ].map(name => Object.values(options.result.children[name].children.styles.children).map(_=>_.value).join('')).join(''),
30+
style: [ 'hit', 'child', 'miss', ].map(
31+
name => Object.values(options.result.children[name].children.styles.children).map(_=>_.value).join('\n')
32+
).join('\n') +'\n'+ options.advanced.children.hideHeader.value,
3133
subPanel: {
3234
title: manifest.name,
3335
url: Runtime.getURL('src/content/embed.html'),
@@ -62,30 +64,53 @@ const classes = {
6264
failed: [ 'tst-search:not-matching', ],
6365
};
6466

65-
messages.addHandler(onSubmit); async function onSubmit({
66-
term, matchCase, wholeWord, regExp,
67+
/**
68+
* Does the actual search on the tabs in a window. Called from the panel via messaging.
69+
* @param {string} options.term The term to search for.
70+
* @param {boolean} options.matchCase See `../common.options.js#model.panel.children.matchCase.input.suffix`.
71+
* @param {boolean} options.wholeWord See `../common.options.js#model.panel.children.wholeWord.input.suffix`.
72+
* @param {boolean} options.regExp See `../common.options.js#model.panel.children.regExp.input.suffix`.
73+
* @param {boolean} options.fieldsPrefix See `../common.options.js#model.search.children.fieldsPrefix.description`.
74+
* @param {number?} options.windowId Optional. The ID of the window to search.
75+
* Defaults to `this?.windowId` (which may be set to the message `sender`) or `Windows.getCurrent().id`.
76+
* @returns
77+
*/
78+
async function onSubmit({
79+
term, matchCase, wholeWord, regExp, fieldsPrefix = options.search.children.fieldsPrefix.value,
6780
windowId = this?.windowId, // eslint-disable-line no-invalid-this
6881
}) { try {
6982
windowId || (windowId = (await Windows.getCurrent()).id);
7083
console.info('TST Search: onSubmit', windowId, this, ...arguments); // eslint-disable-line no-invalid-this
7184

85+
// save search flags
7286
Object.entries({ matchCase, wholeWord, regExp, }).forEach(([ name, value, ]) => {
73-
options.search.children[name].value = value;
87+
options.panel.children[name].value = value;
7488
});
7589

76-
TST.removeTabState({ tabs: '*', state: [].concat(Object.values(classes)), }).catch(onError);
77-
if (!term) { return -1; }
90+
// clear previous search on empty term
91+
if (!term) { TST.removeTabState({ tabs: '*', state: [].concat(Object.values(classes)), }).catch(onError); return -1; }
7892

93+
// pick tab properties to search
94+
const fields = [ 'title', 'url', ];
95+
if (fieldsPrefix) {
96+
const match = (/^(\w+(?:[|]\w+)*): ?(.*)/).exec(term);
97+
if (match) { fields.splice(0, Infinity, ...match[1].split('|')); term = match[2]; }
98+
}
99+
100+
// decide how to search tab properties
79101
let matches; if (regExp) {
80102
if (wholeWord) { term = String.raw`\b(?:${term})\b`; }
81103
const exp = new RegExp(term, matchCase ? '' : 'i');
82-
matches = tab => exp.test(tab.title);
104+
matches = tab => fields.some(key => exp.test(toString(tab[key])));
83105
} else {
84106
const _map = matchCase ? _=>_ :_=>_.toLowerCase();
85107
const map = wholeWord ? _=> ' '+_map(_)+' ' :_=>_map(_);
86108
term = map(term);
87-
matches = tab => typeof tab.title === 'string' && map(tab.title).includes(term);
109+
matches = tab => fields.some(key => map(toString(tab[key])).includes(term));
88110
}
111+
function toString(prop) { return prop == null ? '' : typeof prop === 'string' ? prop : JSON.stringify(prop); }
112+
113+
// get tabs
89114
const [ nativeTabs, treeItems, ] = await Promise.all([
90115
Tabs.query({ windowId, }),
91116
TST.getTree({ window: windowId, }),
@@ -95,13 +120,14 @@ messages.addHandler(onSubmit); async function onSubmit({
95120
return { ...nativeTabs[treeItem.index], ...treeItem, };
96121
};
97122
const tabs = treeItems.map(mergeTabs);
123+
124+
// find search results
98125
const result = {
99126
matching: new Set,
100127
hasChild: new Set,
101128
hidden: new Set,
102129
failed: new Set,
103130
};
104-
105131
(function search(tabs) { return tabs.map(tab => {
106132
if (tab.collapsed || tab.hidden) { result.hidden.add(tab); return false; }
107133
let ret = false; {
@@ -110,14 +136,19 @@ messages.addHandler(onSubmit); async function onSubmit({
110136
} !ret && result.failed.add(tab); return ret;
111137
}).some(_=>_); })(tabs);
112138

113-
(await Object.keys(result).map(state => classes[state].length && TST.addTabState({ tabs: Array.from(result[state], _=>_.id), state: classes[state], })));
139+
// apply findings
140+
(await TST.removeTabState({ tabs: '*', state: [].concat(Object.values(classes)), }).catch(error => void onError(error)));
141+
(await Promise.all(Object.keys(result).map(
142+
state => classes[state].length && result[state].size
143+
&& TST.addTabState({ tabs: Array.from(result[state], _=>_.id), state: classes[state], })
144+
)));
114145

115146
return result.matching.size;
116147

117-
} catch (error) { notify.error('Error in onSubmit', error); return -2; } }
118-
148+
} catch (error) { notify.error('Search failed!', error); return -2; } }
149+
messages.addHandler(onSubmit);
119150
messages.addHandler(function getOptions() {
120-
return Object.fromEntries(Object.entries(options.search.children).map(pair => {
151+
return Object.fromEntries(Object.entries(options.panel.children).map(pair => {
121152
pair[1] = pair[1].value; return pair;
122153
}));
123154
});

src/common/options.js

+49-4
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ const model = {
1313
default: true,
1414
input: { type: 'control', label: `(re-)register with TST`, id: `register`, },
1515
},
16-
search: {
17-
title: 'Search Options',
16+
panel: {
17+
title: 'Search Box Options',
1818
expanded: true,
1919
description: `<small>These are only loaded when the TST sidebar (or a window) is first opened.</small>`,
2020
default: true, children: {
@@ -37,7 +37,8 @@ const model = {
3737
},
3838
},
3939
result: {
40-
title: 'Highlight Results',
40+
title: 'Result Highlighting',
41+
description: `Choose which styles to apply to tabs of the different search result classes.<br>No styles leaves that class unchanged.`,
4142
expanded: true,
4243
default: true, children: {
4344
hit: {
@@ -68,6 +69,9 @@ const model = {
6869
[ 'red', false, 'Red Text', String.raw`
6970
.tab:not(.pinned).tst-search\:child-matching .label { color: red; }
7071
`, ],
72+
[ 'hide', false, 'Hide Completely', String.raw`
73+
.tab.tst-search\:child-matching:not(.tst-search\:matching) { display: none; }
74+
`, ],
7175
]),
7276
},
7377
},
@@ -88,14 +92,55 @@ const model = {
8892
margin-bottom: -13.3px;
8993
transform: scaleY(50%); transform-origin: top;
9094
}
91-
`, ],[ 'hide', false, 'Hide Completely', String.raw`
95+
`, ],
96+
[ 'hide', false, 'Hide Completely', String.raw`
9297
.tab.tst-search\:not-matching { display: none; }
9398
`, ],
9499
]),
95100
},
96101
},
97102
},
98103
},
104+
search: {
105+
title: 'Search Options',
106+
expanded: true,
107+
description: ``,
108+
default: true, children: {
109+
fieldsPrefix: {
110+
title: 'Tab Property Prefixes',
111+
description: String.raw`By default ${manifest.name} will look for the search term, according to the flags set, in the tab's title (the text displayed in the tooltip when holding the mouse cursor the tab) and the URL (the web address displayed at the center top of the window when the tab is active). This should do for most users most of the time.<br>
112+
<details><summary>Most users? Go on ...</summary>
113+
With this option active, the search term can be prefixed with a pipe separated list of tab property names, followed by a colon and an optional space (i.e. matching <code>/^\w+([|]\w+)*: ?/</code>). If such a prefix is found, it is removed from the search term, and the listed properties (converted to strings: empty if <code>null</code>ish, otherwise as JSON (w/o spaces) if not a <code>string</code>) are searched, instead of the default <code>title</code> and <code>url</code>.<br>
114+
This is probably mostly useful for developers. But if one knows what to search for, there is some interesting stuff to be found:<ul>
115+
<li>tabs playing audio: <code>audible: true</code></li>
116+
<li>muted tabs: <code>mutedInfo: "muted":true</code></li>
117+
<li>tabs with SVG favicons: <code>favIconUrl: [./]svg\b</code> (<code>.*</code>)</li>
118+
<li>tabs by container (ID): <code>cookieStoreId: firefox-container-1</code></li>
119+
<li>loaded tabs: <code>discarded: false</code></li>
120+
<li>tabs by ID: <code>id: ^42$</code> (<code>.*</code>)</li>
121+
</ul>
122+
</details>`,
123+
default: false,
124+
input: { type: 'boolean', suffix: `enable field prefixes`, },
125+
},
126+
},
127+
},
128+
advanced: {
129+
title: 'Experimental/Advanced Options',
130+
expanded: false,
131+
description: `Advanced and/or experimental options, that may break and/or disappear at any time. These may also require a reload of TST, this extension or the sidebars to apply.`,
132+
default: true, children: {
133+
hideHeader: {
134+
title: 'Hide Header',
135+
description: `Hides the header above the search, that says something like "${manifest.name}".<br>NOTE: That header is not part of this extension, but of TST itself, and from a UX perspective, should absolutely be there (by default). It may (in the future?) also be used to switch sub panels or do any number of other things. Please DO NOT raise issues about anything loke that with TST while this option is active!`,
136+
default: '',
137+
input: { type: 'boolInt', suffix: `I vow to have read the above and not to annoy TST's authors about it.`, off: '', on: `
138+
#subpanel-container { height: 35px !important; }
139+
#subpanel-header { display: none !important; }
140+
`, },
141+
},
142+
},
143+
},
99144
debug: {
100145
title: 'Debug Level',
101146
expanded: false,

src/views/options.js

+9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@
55
'background/': { register, },
66
}) => async (window, location) => { const { document, } = window;
77

8+
document.head.insertAdjacentHTML('beforeend', `<style>
9+
/* details in checkbox descriptions */
10+
.checkbox-wrapper { vertical-align: top !important; }
11+
.value-suffix details[open] { max-width: calc(100% - 40px); }
12+
13+
/* fix lists in descriptions */
14+
.pref-description li:not(#not) { list-style: unset; margin-left: 6px; }
15+
</style>`);
16+
817
async function onCommand({ name, }, _buttonId) { try { switch (name) {
918
case 'register': {
1019
(await register()); notify.info('Registered', `${manifest.name} should now work!`);

0 commit comments

Comments
 (0)