Skip to content

Commit 5822e40

Browse files
committed
add search bar
1 parent 8719d80 commit 5822e40

6 files changed

Lines changed: 337 additions & 5 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
:root {
2+
--search-bar-bg: #fff;
3+
--search-bar-color: #000;
4+
--search-bar-shadow: rgba(0, 0, 0, 0.3);
5+
--search-input-bg: rgb(240, 240, 240);
6+
--search-input-color: rgb(51, 51, 51);
7+
--search-btn-bg: #fff;
8+
--search-btn-hover-bg: #e0e0e0;
9+
--prev-icon: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2IiBzdHlsZT0iZmlsbDojMDAwOyI+CiAgPHBhdGggZD0iTTUuMzk5OSw4IEM1LjM5OTksNy44NDcgNS40NTg5LDcuNjkzIDUuNTc1OSw3LjU3NiBMOS41NzU5LDMuNTc2IEM5LjgwOTksMy4zNDEgMTAuMTg5OSwzLjM0MSAxMC40MjM5LDMuNTc2IEMxMC42NTg5LDMuODEgMTAuNjU4OSw0LjE5IDEwLjQyMzksNC40MjQgTDYuODQ4OSw4IEwxMC40MjM5LDExLjU3NiBDMTAuNjU4OSwxMS44MSAxMC42NTg5LDEyLjE5IDEwLjQyMzksMTIuNDI0IEMxMC4xODk5LDEyLjY1OSA5LjgwOTksMTIuNjU5IDkuNTc1OSwxMi40MjQgTDUuNTc1OSw4LjQyNCBDNS40NTg5LDguMzA3IDUuMzk5OSw4LjE1MyA1LjM5OTksOCIvPgo8L3N2Zz4K');
10+
--next-icon: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2IiBzdHlsZT0iZmlsbDojMDAwOyI+CiAgPHBhdGggZD0iTTEwLjYwMDEsOCBDMTAuNjAwMSw4LjE1MyAxMC41NDExLDguMzA3IDEwLjQyNDEsOC40MjQgTDYuNDI0MSwxMi40MjQgQzYuMTkwMSwxMi42NTkgNS44MTAxLDEyLjY1OSA1LjU3NjEsMTIuNDI0IEM1LjM0MTEsMTIuMTkgNS4zNDExLDExLjgxIDUuNTc2MSwxMS41NzYgTDkuMTUxMSw4IEw1LjU3NjEsNC40MjQgQzUuMzQxMSw0LjE5IDUuMzQxMSwzLjgxIDUuNTc2MSwzLjU3NiBDNS44MTAxLDMuMzQxIDYuMTkwMSwzLjM0MSA2LjQyNDEsMy41NzYgTDEwLjQyNDEsNy41NzYgQzEwLjU0MTEsNy42OTMgMTAuNjAwMSw3Ljg0NyAxMC42MDAxLDgiLz4KPC9zdmc+Cg==');
11+
--close-icon: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDEyIDEyIiBzdHlsZT0iZmlsbDojMDAwOyI+CiAgPHBhdGggZD0iTTcsNiBMOSw0IEM5LjI3NCwzLjcyNiA5LjI3NCwzLjI3NCA5LDMgQzguNzI2LDIuNzI2IDguMjc0LDIuNzI2IDgsMyBMNiw1IEw0LDMgQzMuNzI2LDIuNzI2IDMuMjc0LDIuNzI2IDMsMyBDMi43MjYsMy4yNzQgMi43MjYsMy43MjYgMyw0IEw1LDYgTDMsOCBDMi43MjYsOC4yNzQgMi43MjYsOC43MjYgMyw5IEMzLjI3NCw5LjI3NCAzLjcyNiw5LjI3NCA0LDkgTDYsNyBMOCw5IEM4LjI3NCw5LjI3NCA4LjcyNiw5LjI3NCA5LDkgQzkuMjc0LDguNzI2IDkuMjc0LDguMjc0IDksOCBMNyw2IFoiLz4KPC9zdmc+Cg==');
12+
}
13+
14+
@media (prefers-color-scheme: dark) {
15+
:root {
16+
--search-bar-bg: #252526;
17+
--search-bar-color: #fff;
18+
--search-bar-shadow: rgba(0, 0, 0, 0.8);
19+
--search-input-bg: rgb(60, 60, 60);
20+
--search-input-color: rgb(204, 204, 204);
21+
--search-btn-bg: #252526;
22+
--search-btn-hover-bg: #666;
23+
--prev-icon: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2IiBzdHlsZT0iZmlsbDojZmZmOyI+CiAgPHBhdGggZD0iTTUuMzk5OSw4IEM1LjM5OTksNy44NDcgNS40NTg5LDcuNjkzIDUuNTc1OSw3LjU3NiBMOS41NzU5LDMuNTc2IEM5LjgwOTksMy4zNDEgMTAuMTg5OSwzLjM0MSAxMC40MjM5LDMuNTc2IEMxMC42NTg5LDMuODEgMTAuNjU4OSw0LjE5IDEwLjQyMzksNC40MjQgTDYuODQ4OSw4IEwxMC40MjM5LDExLjU3NiBDMTAuNjU4OSwxMS44MSAxMC42NTg5LDEyLjE5IDEwLjQyMzksMTIuNDI0IEMxMC4xODk5LDEyLjY1OSA5LjgwOTksMTIuNjU5IDkuNTc1OSwxMi40MjQgTDUuNTc1OSw4LjQyNCBDNS40NTg5LDguMzA3IDUuMzk5OSw4LjE1MyA1LjM5OTksOCIvPgo8L3N2Zz4K');
24+
--next-icon: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2IiBzdHlsZT0iZmlsbDojZmZmOyI+CiAgPHBhdGggZD0iTTEwLjYwMDEsOCBDMTAuNjAwMSw4LjE1MyAxMC41NDExLDguMzA3IDEwLjQyNDEsOC40MjQgTDYuNDI0MSwxMi40MjQgQzYuMTkwMSwxMi42NTkgNS44MTAxLDEyLjY1OSA1LjU3NjEsMTIuNDI0IEM1LjM0MTEsMTIuMTkgNS4zNDExLDExLjgxIDUuNTc2MSwxMS41NzYgTDkuMTUxMSw4IEw1LjU3NjEsNC40MjQgQzUuMzQxMSw0LjE5IDUuMzQxMSwzLjgxIDUuNTc2MSwzLjU3NiBDNS44MTAxLDMuMzQxIDYuMTkwMSwzLjM0MSA2LjQyNDEsMy41NzYgTDEwLjQyNDEsNy41NzYgQzEwLjU0MTEsNy42OTMgMTAuNjAwMSw3Ljg0NyAxMC42MDAxLDgiLz4KPC9zdmc+Cg==');
25+
--close-icon: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDEyIDEyIiBzdHlsZT0iZmlsbDojZmZmOyI+CiAgPHBhdGggZD0iTTcsNiBMOSw0IEM5LjI3NCwzLjcyNiA5LjI3NCwzLjI3NCA5LDMgQzguNzI2LDIuNzI2IDguMjc0LDIuNzI2IDgsMyBMNiw1IEw0LDMgQzMuNzI2LDIuNzI2IDMuMjc0LDIuNzI2IDMsMyBDMi43MjYsMy4yNzQgMi43MjYsMy43MjYgMyw0IEw1LDYgTDMsOCBDMi43MjYsOC4yNzQgMi43MjYsOC43MjYgMyw5IEMzLjI3NCw5LjI3NCAzLjcyNiw5LjI3NCA0LDkgTDYsNyBMOCw5IEM4LjI3NCw5LjI3NCA4LjcyNiw5LjI3NCA5LDkgQzkuMjc0LDguNzI2IDkuMjc0LDguMjc0IDksOCBMNyw2IFoiLz4KPC9zdmc+Cg==');
26+
}
27+
}
28+
29+
.xterm-search-bar__addon {
30+
position: absolute;
31+
max-width: 1467px;
32+
top: 0;
33+
right: 28px;
34+
color: var(--search-bar-color);
35+
background: var(--search-bar-bg);
36+
padding: 5px 10px;
37+
box-shadow: 0 2px 8px var(--search-bar-shadow);
38+
z-index: 999;
39+
display: flex;
40+
}
41+
42+
.xterm-search-bar__addon .search-bar__input {
43+
background-color: var(--search-input-bg);
44+
color: var(--search-input-color);
45+
border: 0;
46+
padding: 2px;
47+
height: 20px;
48+
width: 227px;
49+
}
50+
51+
.xterm-search-bar__addon .search-bar__btn {
52+
min-width: 20px;
53+
width: 20px;
54+
height: 20px;
55+
display: flex;
56+
display: -webkit-flex;
57+
flex: initial;
58+
background-position: 50%;
59+
margin-left: 3px;
60+
background-repeat: no-repeat;
61+
background-color: var(--search-btn-bg);
62+
border: 0;
63+
cursor: pointer;
64+
}
65+
66+
.xterm-search-bar__addon .search-bar__btn:hover {
67+
background-color: var(--search-btn-hover-bg);
68+
}
69+
70+
.xterm-search-bar__addon .search-bar__btn.prev {
71+
margin-left: 20px;
72+
background-image: var(--prev-icon);
73+
}
74+
75+
.xterm-search-bar__addon .search-bar__btn.next {
76+
background-image: var(--next-icon);
77+
}
78+
79+
.xterm-search-bar__addon .search-bar__btn.close {
80+
background-image: var(--close-icon);
81+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { ITerminalAddon, Terminal } from '@xterm/xterm';
2+
import { ISearchOptions, SearchAddon } from '@xterm/addon-search';
3+
import './search.css';
4+
5+
export interface SearchBarOption extends ISearchOptions {
6+
searchAddon: SearchAddon;
7+
}
8+
9+
const ADDON_MARKER_NAME = 'xterm-search-bar__addon';
10+
11+
export class SearchBarAddon implements ITerminalAddon {
12+
private readonly options: Partial<SearchBarOption>;
13+
// @ts-ignore
14+
private terminal: Terminal;
15+
// @ts-ignore
16+
private readonly searchAddon: SearchAddon;
17+
// @ts-ignore
18+
private searchBarElement: HTMLDivElement;
19+
// @ts-ignore
20+
private searchKey: string;
21+
22+
constructor(options: Partial<SearchBarOption>) {
23+
this.options = options || {};
24+
if (this.options && this.options.searchAddon) {
25+
this.searchAddon = this.options.searchAddon;
26+
}
27+
}
28+
29+
activate(terminal: Terminal): void {
30+
this.terminal = terminal;
31+
if (!this.searchAddon) {
32+
console.error('Cannot use search bar addon until search addon has been loaded!');
33+
}
34+
}
35+
36+
dispose() {
37+
this.hidden();
38+
}
39+
40+
/**
41+
* Show the bar in the term
42+
* @returns empty
43+
* @memberof SearchBarAddon necessary search addon instance
44+
*/
45+
show() {
46+
if (!this.terminal || !this.terminal.element) {
47+
console.error('Cannot show search bar addon until terminal has been initialized!');
48+
return;
49+
}
50+
if (this.searchBarElement) {
51+
this.searchBarElement.style.visibility = 'visible';
52+
(this.searchBarElement.querySelector('input') as HTMLInputElement).select();
53+
return;
54+
}
55+
56+
this.terminal.element.style.position = 'relative';
57+
const element = document.createElement('div');
58+
element.innerHTML = `
59+
<input type="text" class="search-bar__input" name="search-bar__input"/>
60+
<button class="search-bar__btn prev"></button>
61+
<button class="search-bar__btn next"></button>
62+
<button class="search-bar__btn close"></button>
63+
`;
64+
element.className = ADDON_MARKER_NAME;
65+
const parentElement = <HTMLElement>this.terminal.element.parentElement;
66+
this.searchBarElement = element;
67+
if (!['relative', 'absoulte', 'fixed'].includes(parentElement.style.position)) {
68+
parentElement.style.position = 'relative';
69+
}
70+
parentElement.appendChild(this.searchBarElement);
71+
this.on('.search-bar__btn.close', 'click', () => {
72+
this.hidden();
73+
});
74+
this.on('.search-bar__btn.next', 'click', () => {
75+
this.searchAddon.findNext(this.searchKey, {
76+
incremental: false
77+
});
78+
});
79+
this.on('.search-bar__btn.prev', 'click', () => {
80+
this.searchAddon.findPrevious(this.searchKey, {
81+
incremental: false
82+
});
83+
});
84+
this.on('.search-bar__input', 'keyup', (e: any) => {
85+
this.searchKey = (e.target as HTMLInputElement).value;
86+
this.searchAddon.findNext(this.searchKey, {
87+
incremental: e.key !== `Enter`
88+
});
89+
});
90+
(this.searchBarElement.querySelector('input') as HTMLInputElement).select();
91+
}
92+
93+
/**
94+
* You can manually call close, also can click the close button on the bar
95+
* @memberof SearchBarAddon
96+
*/
97+
hidden() {
98+
if (this.searchBarElement && (this.terminal.element as HTMLElement).parentElement) {
99+
this.searchBarElement.style.visibility = 'hidden';
100+
}
101+
}
102+
103+
private on(selector: string, event: string, cb: (e: any) => void) {
104+
const parentElement = <HTMLElement>(this.terminal.element as HTMLElement).parentElement;
105+
parentElement.addEventListener(event, (e) => {
106+
let target = e.target;
107+
108+
while (target !== document.querySelector(selector)) {
109+
if (target === parentElement) {
110+
target = null;
111+
break;
112+
}
113+
114+
target = (target as HTMLElement).parentElement;
115+
}
116+
117+
if (target === document.querySelector(selector)) {
118+
cb.call(this, e);
119+
e.stopPropagation();
120+
}
121+
});
122+
}
123+
124+
/**
125+
* You can customize your own style, and then add CSS string template after search bar init
126+
* @param {string} newStyle
127+
* @memberof SearchBarAddon
128+
*/
129+
addNewStyle(newStyle: string) {
130+
let styleElement = document.getElementById(ADDON_MARKER_NAME) as HTMLStyleElement;
131+
132+
if (!styleElement) {
133+
styleElement = document.createElement('style');
134+
styleElement.type = 'text/css';
135+
styleElement.id = ADDON_MARKER_NAME;
136+
document.getElementsByTagName('head')[0].appendChild(styleElement);
137+
}
138+
139+
styleElement.appendChild(document.createTextNode(newStyle));
140+
}
141+
}

extension/entrypoints/shared/term.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Terminal } from "@xterm/xterm";
22
import { FitAddon } from "@xterm/addon-fit";
33
import { AttachAddon } from "@xterm/addon-attach";
4+
import { SearchAddon } from '@xterm/addon-search'
5+
import { SearchBarAddon } from "./search"
46
import { WebglAddon } from "@xterm/addon-webgl";
57
import { WebLinksAddon } from "@xterm/addon-web-links";
68
import { RequestCreateTTY, RequestGetXtermConfig, RequestResizeTTY, ResponseCreateTTY, ResponseGetXtermConfig } from "./rpc";
@@ -50,12 +52,18 @@ async function main() {
5052
}
5153

5254
const terminal = new Terminal(xtermResp.result);
53-
const fitAddon = new FitAddon();
5455

56+
const fitAddon = new FitAddon();
5557
terminal.loadAddon(fitAddon);
58+
5659
terminal.loadAddon(new WebglAddon());
5760
terminal.loadAddon(new WebLinksAddon());
5861

62+
const searchAddon = new SearchAddon();
63+
const searchBarAddon = new SearchBarAddon({ searchAddon });
64+
terminal.loadAddon(searchAddon);
65+
terminal.loadAddon(searchBarAddon)
66+
5967
const ws = new WebSocket(resp.result.url);
6068
const attachAddon = new AttachAddon(ws);
6169
terminal.loadAddon(attachAddon);
@@ -101,6 +109,32 @@ async function main() {
101109
terminal.focus();
102110
};
103111

112+
113+
// show the search bar on cmd+f on macOS or ctrl+f on other platforms
114+
let searchBarIsVisible = false;
115+
globalThis.addEventListener("keydown", (event) => {
116+
console.log("Key pressed:", event.key, "Meta:", event.metaKey, "Ctrl:", event.ctrlKey);
117+
if ((event.metaKey || event.ctrlKey) && event.key === "f") {
118+
event.preventDefault();
119+
120+
if (searchBarIsVisible) {
121+
searchBarAddon.hidden();
122+
searchBarIsVisible = false;
123+
return;
124+
}
125+
126+
searchBarAddon.show()
127+
searchBarIsVisible = true;
128+
return;
129+
}
130+
131+
if (event.key === "Escape" && searchBarIsVisible) {
132+
searchBarAddon.hidden();
133+
searchBarIsVisible = false;
134+
return;
135+
}
136+
})
137+
104138
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", async (event) => {
105139
const variant = event.matches ? "dark" : "light";
106140
const resp = await browser.runtime.sendMessage<RequestGetXtermConfig, ResponseGetXtermConfig>({

0 commit comments

Comments
 (0)