ui: add support find in terminal

Signed-off-by: Jianhui Zhao <zhaojh329@gmail.com>
This commit is contained in:
Jianhui Zhao
2025-07-26 15:31:09 +08:00
parent 0bf9eff0ab
commit f54f5323bd
5 changed files with 149 additions and 3 deletions
+10
View File
@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-search": "^0.15.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"element-plus": "^2.10.1",
@@ -1820,6 +1821,15 @@
"@xterm/xterm": "^5.0.0"
}
},
"node_modules/@xterm/addon-search": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.15.0.tgz",
"integrity": "sha512-ZBZKLQ+EuKE83CqCmSSz5y1tx+aNOCUaA7dm6emgOX+8J9H1FWXZyrKfzjwzV+V14TV3xToz1goIeRhXBS5qjg==",
"license": "MIT",
"peerDependencies": {
"@xterm/xterm": "^5.0.0"
}
},
"node_modules/@xterm/addon-web-links": {
"version": "0.11.0",
"resolved": "https://registry.npmmirror.com/@xterm/addon-web-links/-/addon-web-links-0.11.0.tgz",
+1
View File
@@ -10,6 +10,7 @@
},
"dependencies": {
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-search": "^0.15.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"element-plus": "^2.10.1",
+122 -1
View File
@@ -13,18 +13,32 @@
<el-button type="primary" @click="doUploadFile">{{ $t('OK') }}</el-button>
</template>
</el-dialog>
<transition name="find-box">
<div v-if="showFindBox" class="find-box" @keydown="onFindBoxKeydown">
<el-input ref="findInput" class="find-input" v-model="findText" :placeholder="$t('Find')" clearable @keydown.enter="doFind('next')"/>
<el-checkbox-group class="find-flags" v-model="findFlags">
<el-checkbox value="caseSensitive" :label="$t('Match Case')"/>
<el-checkbox value="wholeWord" :label="$t('Whole Word')"/>
<el-checkbox value="regex" :label="$t('Regular')"/>
</el-checkbox-group>
<el-button size="small" @click="doFind('prev')">{{ $t('Prev') }}</el-button>
<el-button size="small" @click="doFind('next')">{{ $t('Next') }}</el-button>
<el-button size="small" @click="showFindBox = false">{{ $t('Close') }}</el-button>
</div>
</transition>
<ContextMenu v-model="contextmenuPos" :menus="contextmenus" @click="onContextmenuClick"/>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted, nextTick, useTemplateRef } from 'vue'
import { ref, reactive, onMounted, onUnmounted, nextTick, useTemplateRef, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus'
import useClipboard from 'vue-clipboard3'
import { Terminal } from '@xterm/xterm'
import { FitAddon } from '@xterm/addon-fit'
import { SearchAddon } from '@xterm/addon-search'
import { WebLinksAddon } from '@xterm/addon-web-links'
import '@xterm/xterm/css/xterm.css'
import OverlayAddon from '../xterm-addon/xterm-addon-overlay'
@@ -59,6 +73,8 @@ const contextmenus = [
{name: 'copy', caption: t('Copy - Ctrl+Insert')},
{name: 'paste', caption: t('Paste - Shift+Insert')},
{name: 'clear', caption: t('Clear Scrollback')},
{name: 'find', caption: t('Find') + ' - Ctrl+F'},
{name: 'clear-highlighting', caption: t('Clear Highlighting')},
{name: 'font+', caption: t('font+')},
{name: 'font-', caption: t('font-')},
{name: 'upload', caption: t('Upload file') + ' - rtty -R'},
@@ -71,6 +87,28 @@ const contextmenus = [
{name: 'about', caption: t('About')}
]
const showFindBox = ref(false)
const findInput = useTemplateRef('findInput')
const findText = ref('')
const findFlags = ref([])
const openFindBox = () => {
showFindBox.value = true
nextTick(() => findInput.value.focus())
}
const onFindBoxKeydown = (e) => {
if (e.key === 'Escape') {
showFindBox.value = false
}
}
watch(() => showFindBox.value, (val) => {
if (!val) {
nextTick(() => term.focus())
}
})
const fileCtx = reactive({
modal: false,
accepted: false,
@@ -85,6 +123,7 @@ let disposables = []
let socket = null
let term = null
let fitAddon = null
let searchAddon = null
let unack = 0
const showKeyboard = ref(false)
const isConnected = ref(false)
@@ -115,6 +154,12 @@ const onContextmenuClick = (name) => {
pasteFromClipboard()
} else if (name === 'clear') {
term.clear()
} else if (name === 'find') {
openFindBox()
return
} else if (name === 'clear-highlighting') {
searchAddon.clearDecorations()
term.clearSelection()
} else if (name === 'font+') {
updateFontSize(1)
} else if (name === 'font-') {
@@ -168,6 +213,28 @@ const updateFontSize = (size) => {
fitAddon.fit()
}
const doFind = (type) => {
const options = {
decorations: {
matchBackground: '#2e7d32',
matchBorder: '#2e7d32',
matchOverviewRuler: '#2e7d32',
activeMatchBackground: '#ff8f00',
activeMatchBorder: '#ff8f00',
activeMatchColorOverviewRuler: '#ff8f00'
}
}
findFlags.value.forEach(v => {
options[v] = true
})
if (type === 'next')
searchAddon.findNext(findText.value, options)
else
searchAddon.findPrevious(findText.value, options)
}
const onUploadDialogClosed = () => {
term.focus()
if (fileCtx.accepted)
@@ -252,6 +319,7 @@ const closed = () => {
const openTerm = () => {
term = new Terminal({
allowProposedApi: true,
cursorBlink: true,
cursorStyle: 'bar',
cursorInactiveStyle: 'none',
@@ -263,6 +331,9 @@ const openTerm = () => {
fitAddon = new FitAddon()
term.loadAddon(fitAddon)
searchAddon = new SearchAddon()
term.loadAddon(searchAddon)
const overlayAddon = new OverlayAddon()
term.loadAddon(overlayAddon)
@@ -278,6 +349,13 @@ const openTerm = () => {
overlayAddon.show(term.cols + 'x' + term.rows)
}))
disposables.push(term.onKey(({ domEvent }) => {
const e = domEvent
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'f') {
openFindBox()
}
}))
window.addEventListener('rtty-resize', fitTerm)
fitTerm()
nextTick(() => term.focus())
@@ -443,4 +521,47 @@ onUnmounted(() => {
.keyboard-toggle-btn:hover {
opacity: 1;
}
.find-box {
width: auto;
position: absolute;
top: 5px;
right: 15px;
z-index: 1001;
background: rgba(255,255,255,0.98);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
padding: 4px 4px;
display: flex;
align-items: center;
gap: 5px;
}
.find-box-enter-active,
.find-box-leave-active {
transition: all 0.3s cubic-bezier(.55,0,.1,1);
}
.find-box-enter-from,
.find-box-leave-to {
opacity: 0;
transform: translateY(-40px);
}
.find-box-enter-to,
.find-box-leave-from {
opacity: 1;
transform: translateY(0);
}
.find-input {
width: 200px;
}
.find-flags {
display: flex;
gap: 5px
}
:deep(.el-checkbox) {
margin-right: 1px;
}
</style>
+8 -1
View File
@@ -89,5 +89,12 @@
"Requesting device to create terminal...": "Requesting device to create terminal...",
"Clipboard Permission Required": "Clipboard Permission Required",
"clipboard_instructions": "To enable clipboard permissions, click the site information icon on the left of address bar to access permission settings, or configure clipboard permissions for this site in browser privacy settings. You can also use Shift+Insert keyboard shortcut to paste.",
"Virtual Keyboard": "Virtual Keyboard"
"Virtual Keyboard": "Virtual Keyboard",
"Find": "Find",
"Prev": "Prev",
"Next": "Next",
"Match Case": "Match Case",
"Whole Word": "Whole Word",
"Regular": "Regular",
"Clear Highlighting": "Clear Highlighting"
}
+8 -1
View File
@@ -89,5 +89,12 @@
"Requesting device to create terminal...": "正在请求设备创建终端...",
"Clipboard Permission Required": "需要剪切板权限",
"clipboard_instructions": "要启用剪切板权限,请点击地址栏左侧的站点信息图标进入权限设置,或在浏览器设置的隐私安全选项中配置此网站的剪切板权限。也可使用快捷键 Shift+Insert 进行粘贴。",
"Virtual Keyboard": "虚拟键盘"
"Virtual Keyboard": "虚拟键盘",
"Find": "查找",
"Prev": "上一个",
"Next": "下一个",
"Match Case": "匹配大小写",
"Whole Word": "匹配整个单词",
"Regular": "正则匹配",
"Clear Highlighting": "清除高亮"
}