adds terminal search support (#395)

pull/398/head
Dane Roelofs 2023-03-30 21:44:00 -07:00 committed by GitHub
parent 48820cdd10
commit a0bba782a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 126 additions and 11 deletions

33
package-lock.json generated
View File

@ -91,6 +91,7 @@
"watcher": "^2.2.2",
"webpack": "^5.75.0",
"xterm": "^4.19.0",
"xterm-addon-search": "^0.9.0",
"xterm-addon-web-links": "^0.6.0"
},
"devDependencies": {
@ -2953,6 +2954,15 @@
"node": ">= 14.17.5"
}
},
"node_modules/@electron-forge/web-multi-logger/node_modules/xterm-addon-search": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/xterm-addon-search/-/xterm-addon-search-0.8.2.tgz",
"integrity": "sha512-I1863mjn8P6uVrqm/X+btalVsqjAKLhnhpbP7SavAOpEkI1jJhbHU2UTp7NjeRtcKTks6UWk/ycgds5snDSejg==",
"dev": true,
"peerDependencies": {
"xterm": "^4.0.0"
}
},
"node_modules/@electron/asar": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.3.tgz",
@ -23634,10 +23644,9 @@
}
},
"node_modules/xterm-addon-search": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/xterm-addon-search/-/xterm-addon-search-0.8.2.tgz",
"integrity": "sha512-I1863mjn8P6uVrqm/X+btalVsqjAKLhnhpbP7SavAOpEkI1jJhbHU2UTp7NjeRtcKTks6UWk/ycgds5snDSejg==",
"dev": true,
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/xterm-addon-search/-/xterm-addon-search-0.9.0.tgz",
"integrity": "sha512-aoolI8YuHvdGw+Qjg8g2M4kst0v86GtB7WeBm4F0jNXA005/6QbWWy9eCsvnIDLJOFI5JSSrZnD6CaOkvBQYPA==",
"peerDependencies": {
"xterm": "^4.0.0"
}
@ -25849,6 +25858,15 @@
"xterm": "^4.9.0",
"xterm-addon-fit": "^0.5.0",
"xterm-addon-search": "^0.8.0"
},
"dependencies": {
"xterm-addon-search": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/xterm-addon-search/-/xterm-addon-search-0.8.2.tgz",
"integrity": "sha512-I1863mjn8P6uVrqm/X+btalVsqjAKLhnhpbP7SavAOpEkI1jJhbHU2UTp7NjeRtcKTks6UWk/ycgds5snDSejg==",
"dev": true,
"requires": {}
}
}
},
"@electron/asar": {
@ -41287,10 +41305,9 @@
"requires": {}
},
"xterm-addon-search": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/xterm-addon-search/-/xterm-addon-search-0.8.2.tgz",
"integrity": "sha512-I1863mjn8P6uVrqm/X+btalVsqjAKLhnhpbP7SavAOpEkI1jJhbHU2UTp7NjeRtcKTks6UWk/ycgds5snDSejg==",
"dev": true,
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/xterm-addon-search/-/xterm-addon-search-0.9.0.tgz",
"integrity": "sha512-aoolI8YuHvdGw+Qjg8g2M4kst0v86GtB7WeBm4F0jNXA005/6QbWWy9eCsvnIDLJOFI5JSSrZnD6CaOkvBQYPA==",
"requires": {}
},
"xterm-addon-web-links": {

View File

@ -172,6 +172,7 @@
"watcher": "^2.2.2",
"webpack": "^5.75.0",
"xterm": "^4.19.0",
"xterm-addon-search": "^0.9.0",
"xterm-addon-web-links": "^0.6.0"
}
}

View File

@ -2,6 +2,7 @@ import React, { useEffect, useRef } from 'react'
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
import { WebLinksAddon } from 'xterm-addon-web-links'
import { SearchAddon } from 'xterm-addon-search'
import 'xterm/css/xterm.css'
import { useAppDispatch, useAppSelector } from '../app/hooks'
import { FullState } from '../features/window/state'
@ -9,9 +10,11 @@ import * as gs from '../features/globalSlice'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faTimes } from '@fortawesome/free-solid-svg-icons'
import { throttleCallback } from './componentUtils'
import { faChevronDown, faChevronUp } from '@fortawesome/pro-regular-svg-icons'
export function XTermComponent({ height }: { height: number }) {
const terminalRef = useRef<HTMLDivElement>(null)
const searchBarInputRef = useRef<HTMLInputElement>(null)
const terminal = useRef<Terminal | null>(null)
const fitAddon = useRef<FitAddon>(new FitAddon())
const webLinksAddon = useRef<WebLinksAddon>(
@ -20,6 +23,8 @@ export function XTermComponent({ height }: { height: number }) {
connector.terminalClickLink(url)
})
)
const searchAddon = useRef<SearchAddon>(new SearchAddon())
const [searchBarOpen, setSearchBarOpen] = React.useState(false)
const handleIncomingData = (e: any, data: any) => {
terminal.current!.write(data)
@ -38,6 +43,7 @@ export function XTermComponent({ height }: { height: number }) {
terminal.current.loadAddon(fitAddon.current)
terminal.current.loadAddon(webLinksAddon.current)
terminal.current.loadAddon(searchAddon.current)
if (terminalRef.current) {
terminal.current.open(terminalRef.current)
@ -49,6 +55,17 @@ export function XTermComponent({ height }: { height: number }) {
connector.terminalInto(e)
})
terminal.current.attachCustomKeyEventHandler((e) => {
if (e.ctrlKey && e.key === 'f') {
openSearchBar()
return false
} else if (e.key === 'Escape') {
closeSearchBar()
return false
}
return true
})
connector.registerIncData(handleIncomingData)
// Make the terminal's size and geometry fit the size of #terminal-container
@ -68,12 +85,77 @@ export function XTermComponent({ height }: { height: number }) {
}
}, [height, terminal, fitAddon])
const openSearchBar = () => {
setSearchBarOpen(true)
searchBarInputRef.current?.focus()
}
const closeSearchBar = () => {
setSearchBarOpen(false)
terminal.current?.focus()
}
const findNextSearchResult = () => {
if (searchBarInputRef.current?.value) {
searchAddon.current.findNext(searchBarInputRef.current.value)
}
}
const findPreviousSearchResult = () => {
if (searchBarInputRef.current?.value) {
searchAddon.current.findPrevious(searchBarInputRef.current.value)
}
}
return (
<div
className="terminalInnerContainer"
ref={terminalRef}
style={{ height: height + 'px' }}
></div>
style={{ height: height + 'px', position: 'relative' }}
>
{searchBarOpen && (
<div
className="search-input flex justify-end absolute top-1 right-4 z-10 md:w-80"
onKeyDown={(e) => {
if (e.key === 'Escape') {
closeSearchBar()
}
}}
>
<input
className="search-input w-full"
placeholder="Search..."
autoFocus
ref={searchBarInputRef}
onKeyDown={(e) => {
if (e.key === 'Enter') {
findNextSearchResult()
}
}}
/>
<div className="flex">
<button
className="icon"
onClick={() => findPreviousSearchResult()}
>
<FontAwesomeIcon icon={faChevronUp} />
</button>
<button
className="icon"
onClick={() => findNextSearchResult()}
>
<FontAwesomeIcon icon={faChevronDown} />
</button>
<button
className="icon"
onClick={() => closeSearchBar()}
>
<FontAwesomeIcon icon={faTimes} />
</button>
</div>
</div>
)}
</div>
)
}
export const BottomTerminal: React.FC = () => {

View File

@ -1517,6 +1517,13 @@ cm-diff-cancel > div {
padding: 12px;
}
.search-input {
border-radius: 5px;
background-color: var(--input-background);
color: var(--input-text);
padding: 4px;
}
/* .cm-diagnostic {
display: block;
word-break: keep-all;
@ -2416,6 +2423,14 @@ free servers .disabled_command .shortcut__block {
align-items: center;
}
.terminalInnerContainer .icon {
color: #bbb;
font-size: 18px;
margin: auto;
width: 24px;
height: 24px;
}
.terminalTitle {
color: #bbb;
flex-grow: 1;