Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70c33a5572 | ||
|
|
7f2e4c1110 | ||
|
|
7d3057ee5d | ||
|
|
2b3fefafd0 | ||
|
|
8f686f56ca | ||
|
|
c3baac3f05 |
No files matched your search
@@ -25,12 +25,18 @@ Electron + electron-vite + React 终端工具(本地终端 / SSH / SFTP)。
|
|||||||
1. `package.json` 版本号 +1,写 `RELEASE_NOTES.md`(仓库根,已 gitignore)
|
1. `package.json` 版本号 +1,写 `RELEASE_NOTES.md`(仓库根,已 gitignore)
|
||||||
2. `npm run dist` 构建
|
2. `npm run dist` 构建
|
||||||
3. `node scripts/release.cjs <版本号>`,例如 `node scripts/release.cjs 1.0.2`
|
3. `node scripts/release.cjs <版本号>`,例如 `node scripts/release.cjs 1.0.2`
|
||||||
- 依次完成:Gitea release(含 msi/exe 资产)→ Gitea 更新通道(`api/packages/admin/generic/openterminal-update/stable`,更新 latest.yml/blockmap/exe)→ GitHub release
|
- 依次完成:Gitea release(含 msi/exe 资产)→ Gitea 更新通道(`api/packages/admin/generic/openterminal-update/stable`,更新 latest.yml/blockmap/exe/release-notes.md)→ GitHub release
|
||||||
- 可用 `--skip-github` / `--skip-gitea` 跳过某步
|
- 可用 `--skip-github` / `--skip-gitea` 跳过某步
|
||||||
- 访问 GitHub API 不通时,设 `HTTPS_PROXY=http://127.0.0.1:7897` 再走代理
|
- 访问 GitHub API 不通时,设 `HTTPS_PROXY=http://127.0.0.1:7897` 再走代理
|
||||||
4. 验证更新通道:`curl https://git.codingplan.site/api/packages/admin/generic/openterminal-update/stable/latest.yml` 应返回新版本号
|
4. 验证更新通道:`curl https://git.codingplan.site/api/packages/admin/generic/openterminal-update/stable/latest.yml` 应返回新版本号
|
||||||
5. `git tag v<版本号>` 并推送两个远程
|
5. `git tag v<版本号>` 并推送两个远程
|
||||||
|
|
||||||
|
## 更新机制
|
||||||
|
|
||||||
|
- 检查更新:先 Gitea 更新通道(强制直连,不走系统代理),失败回退 GitHub(走系统代理)。 electron-updater 用独立 session(partition `electron-updater`),代理模式在 `useFeed` 里按源切换
|
||||||
|
- 更新日志:Gitea 仓库是私有的(匿名 API 404),改为从更新通道的 `release-notes.md` 读取(直连 session `openterminal-update-direct`),再回退 Gitea/GitHub releases API
|
||||||
|
- electron-updater 不支持 MSI 自动更新,自动更新只走 NSIS exe
|
||||||
|
|
||||||
## 主题机制
|
## 主题机制
|
||||||
|
|
||||||
- 终端主题由 xterm 主题派生 UI 配色:`src/renderer/src/theme/chrome.ts` 的 `applyChromeTheme` 写入 `--chrome-bg/-bg-deep/-border/-hover` CSS 变量,antd token 在 `main.tsx` ThemedConfigProvider 派生
|
- 终端主题由 xterm 主题派生 UI 配色:`src/renderer/src/theme/chrome.ts` 的 `applyChromeTheme` 写入 `--chrome-bg/-bg-deep/-border/-hover` CSS 变量,antd token 在 `main.tsx` ThemedConfigProvider 派生
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "open-terminal",
|
"name": "open-terminal",
|
||||||
"productName": "OpenTerminal",
|
"productName": "OpenTerminal",
|
||||||
"version": "1.0.2",
|
"version": "1.0.4",
|
||||||
"description": "Open-source terminal with SSH, split panes, themes and fonts",
|
"description": "Open-source terminal with SSH, split panes, themes and fonts",
|
||||||
"main": "out/main/index.js",
|
"main": "out/main/index.js",
|
||||||
"author": "Bill",
|
"author": "Bill",
|
||||||
|
|||||||
+9
-2
@@ -75,8 +75,15 @@ async function giteaChannel() {
|
|||||||
const base = 'https://git.codingplan.site/api/packages/admin/generic/openterminal-update/stable'
|
const base = 'https://git.codingplan.site/api/packages/admin/generic/openterminal-update/stable'
|
||||||
const del = await fetch(base, { method: 'DELETE', headers: { Authorization: `token ${env.GIT_TOKEN}` } })
|
const del = await fetch(base, { method: 'DELETE', headers: { Authorization: `token ${env.GIT_TOKEN}` } })
|
||||||
console.log(' delete old stable:', del.status)
|
console.log(' delete old stable:', del.status)
|
||||||
for (const f of [path.join(R, 'latest.yml'), path.join(R, `OpenTerminal-${V}-setup.exe.blockmap`), path.join(R, `OpenTerminal-${V}-setup.exe`)]) {
|
const channelFiles = [
|
||||||
const name = path.basename(f)
|
[path.join(R, 'latest.yml'), 'latest.yml'],
|
||||||
|
[path.join(R, `OpenTerminal-${V}-setup.exe.blockmap`), `OpenTerminal-${V}-setup.exe.blockmap`],
|
||||||
|
[path.join(R, `OpenTerminal-${V}-setup.exe`), `OpenTerminal-${V}-setup.exe`],
|
||||||
|
// Changelog served from the channel: the Gitea repo is private (anonymous
|
||||||
|
// releases API 404s), but the generic package is publicly readable.
|
||||||
|
[path.join(ROOT, 'RELEASE_NOTES.md'), 'release-notes.md']
|
||||||
|
]
|
||||||
|
for (const [f, name] of channelFiles) {
|
||||||
const stat = fs.statSync(f)
|
const stat = fs.statSync(f)
|
||||||
const resp = await fetch(`${base}/${encodeURIComponent(name)}`, {
|
const resp = await fetch(`${base}/${encodeURIComponent(name)}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ process.on('uncaughtException', (err) => {
|
|||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
registerIpc()
|
registerIpc()
|
||||||
|
registerUpdateIpc()
|
||||||
// OS-level effects (login item, sleep blocker) must apply even if the
|
// OS-level effects (login item, sleep blocker) must apply even if the
|
||||||
// settings dialog is never opened this run.
|
// settings dialog is never opened this run.
|
||||||
applyStartupSystemSettings(loadSettings())
|
applyStartupSystemSettings(loadSettings())
|
||||||
|
|||||||
+2
-2
@@ -34,7 +34,7 @@ function showMainWindow(showOrCreate: () => void): void {
|
|||||||
/** Rebuild the context menu so the close-action radio items reflect settings. */
|
/** Rebuild the context menu so the close-action radio items reflect settings. */
|
||||||
function refreshContextMenu(showOrCreate: () => void): void {
|
function refreshContextMenu(showOrCreate: () => void): void {
|
||||||
if (!tray) return
|
if (!tray) return
|
||||||
const action = loadSettings().system.closeAction ?? 'ask'
|
const action = loadSettings().system.closeAction ?? 'tray'
|
||||||
const setAction = (value: CloseAction) => (): void => {
|
const setAction = (value: CloseAction) => (): void => {
|
||||||
persistCloseAction(value)
|
persistCloseAction(value)
|
||||||
refreshContextMenu(showOrCreate)
|
refreshContextMenu(showOrCreate)
|
||||||
@@ -86,7 +86,7 @@ function hideToTray(win: BrowserWindow): void {
|
|||||||
*/
|
*/
|
||||||
export async function onMainWindowClose(win: BrowserWindow, e: Electron.Event, showOrCreate: () => void): Promise<void> {
|
export async function onMainWindowClose(win: BrowserWindow, e: Electron.Event, showOrCreate: () => void): Promise<void> {
|
||||||
if (quitting) return
|
if (quitting) return
|
||||||
const action = loadSettings().system.closeAction ?? 'ask'
|
const action = loadSettings().system.closeAction ?? 'tray'
|
||||||
if (action === 'tray') {
|
if (action === 'tray') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
hideToTray(win)
|
hideToTray(win)
|
||||||
|
|||||||
+40
-3
@@ -1,4 +1,4 @@
|
|||||||
import { app, ipcMain, net } from 'electron'
|
import { app, ipcMain, net, session } from 'electron'
|
||||||
import { autoUpdater } from 'electron-updater'
|
import { autoUpdater } from 'electron-updater'
|
||||||
import { Ipc, type ReleaseNote, type UpdateState } from '../shared/ipc'
|
import { Ipc, type ReleaseNote, type UpdateState } from '../shared/ipc'
|
||||||
import { broadcast } from './broadcast'
|
import { broadcast } from './broadcast'
|
||||||
@@ -30,6 +30,8 @@ function setState(patch: Partial<UpdateState>): void {
|
|||||||
function useFeed(feed: 'gitea' | 'github'): void {
|
function useFeed(feed: 'gitea' | 'github'): void {
|
||||||
activeFeed = feed
|
activeFeed = feed
|
||||||
if (feed === 'gitea') {
|
if (feed === 'gitea') {
|
||||||
|
// Domestic feed: always direct — a system proxy only breaks it.
|
||||||
|
void autoUpdater.netSession.setProxy({ mode: 'direct' })
|
||||||
const token = process.env.OT_UPDATE_TOKEN
|
const token = process.env.OT_UPDATE_TOKEN
|
||||||
autoUpdater.setFeedURL({
|
autoUpdater.setFeedURL({
|
||||||
provider: 'generic',
|
provider: 'generic',
|
||||||
@@ -37,6 +39,8 @@ function useFeed(feed: 'gitea' | 'github'): void {
|
|||||||
...(token ? { requestHeaders: { Authorization: `token ${token}` } } : {})
|
...(token ? { requestHeaders: { Authorization: `token ${token}` } } : {})
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
// GitHub usually needs the system proxy (when one is enabled).
|
||||||
|
void autoUpdater.netSession.setProxy({ mode: 'system' })
|
||||||
autoUpdater.setFeedURL({ provider: 'github', ...GITHUB_REPO })
|
autoUpdater.setFeedURL({ provider: 'github', ...GITHUB_REPO })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,8 +71,14 @@ async function checkWithFallback(): Promise<void> {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (activeFeed !== 'gitea') throw err
|
if (activeFeed !== 'gitea') throw err
|
||||||
console.warn('[updater] gitea feed failed, falling back to github:', err)
|
console.warn('[updater] gitea feed failed, falling back to github:', err)
|
||||||
|
const giteaErr = err instanceof Error ? err.message : String(err)
|
||||||
useFeed('github')
|
useFeed('github')
|
||||||
await autoUpdater.checkForUpdates()
|
try {
|
||||||
|
await autoUpdater.checkForUpdates()
|
||||||
|
} catch (err2) {
|
||||||
|
const ghErr = err2 instanceof Error ? err2.message : String(err2)
|
||||||
|
throw new Error(`国内源: ${giteaErr};GitHub: ${ghErr}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,8 +106,35 @@ async function handleDownload(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gitea releases first, GitHub releases as fallback; both are public reads. */
|
/** Direct-connection fetch for the domestic update channel (ignores system proxy). */
|
||||||
|
async function directFetch(url: string): Promise<Response> {
|
||||||
|
const s = session.fromPartition('openterminal-update-direct', { cache: false })
|
||||||
|
await s.setProxy({ mode: 'direct' })
|
||||||
|
return s.fetch(url, { headers: { 'User-Agent': 'OpenTerminal' } })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changelog sources, in order:
|
||||||
|
* 1. update channel's release-notes.md — the Gitea repo itself is private
|
||||||
|
* (anonymous API reads 404), but the generic package is publicly readable.
|
||||||
|
* 2. Gitea / GitHub releases APIs (full history when reachable).
|
||||||
|
*/
|
||||||
async function fetchChangelog(): Promise<ReleaseNote[]> {
|
async function fetchChangelog(): Promise<ReleaseNote[]> {
|
||||||
|
try {
|
||||||
|
const [notesResp, ymlResp] = await Promise.all([
|
||||||
|
directFetch(`${GITEA_FEED}release-notes.md`),
|
||||||
|
directFetch(`${GITEA_FEED}latest.yml`)
|
||||||
|
])
|
||||||
|
if (notesResp.ok) {
|
||||||
|
const body = await notesResp.text()
|
||||||
|
const yml = ymlResp.ok ? await ymlResp.text() : ''
|
||||||
|
const version = yml.match(/^version:\s*(\S+)/m)?.[1] ?? ''
|
||||||
|
const date = yml.match(/^releaseDate:\s*'?(\S+?)'?$/m)?.[1]?.slice(0, 10) ?? ''
|
||||||
|
if (body.trim()) return [{ version: version ? `v${version}` : 'latest', date, body }]
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// fall through to releases APIs
|
||||||
|
}
|
||||||
for (const url of [GITEA_RELEASES_API, GITHUB_RELEASES_API]) {
|
for (const url of [GITEA_RELEASES_API, GITHUB_RELEASES_API]) {
|
||||||
try {
|
try {
|
||||||
const resp = await net.fetch(url, { headers: { 'User-Agent': 'OpenTerminal' } })
|
const resp = await net.fetch(url, { headers: { 'User-Agent': 'OpenTerminal' } })
|
||||||
|
|||||||
@@ -317,6 +317,22 @@ export function SystemSettingsTab(): React.JSX.Element {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<SettingRow
|
||||||
|
label="关闭按钮行为"
|
||||||
|
desc="点击窗口关闭按钮时的动作;最小化到托盘后可在托盘图标右键菜单中退出"
|
||||||
|
control={
|
||||||
|
<Select
|
||||||
|
className="settings-select"
|
||||||
|
value={settings.system.closeAction ?? 'tray'}
|
||||||
|
onChange={(value) => void updateSystem({ closeAction: value })}
|
||||||
|
options={[
|
||||||
|
{ value: 'tray', label: '最小化到托盘' },
|
||||||
|
{ value: 'exit', label: '直接退出' },
|
||||||
|
{ value: 'ask', label: '每次询问' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<SettingRow
|
<SettingRow
|
||||||
label="全局唤起快捷键"
|
label="全局唤起快捷键"
|
||||||
desc="Control+Shift+Alt+T 等,留空禁用"
|
desc="Control+Shift+Alt+T 等,留空禁用"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { FitAddon } from '@xterm/addon-fit'
|
|||||||
import { WebglAddon } from '@xterm/addon-webgl'
|
import { WebglAddon } from '@xterm/addon-webgl'
|
||||||
import { SearchAddon } from '@xterm/addon-search'
|
import { SearchAddon } from '@xterm/addon-search'
|
||||||
import '@xterm/xterm/css/xterm.css'
|
import '@xterm/xterm/css/xterm.css'
|
||||||
import { FolderOpenOutlined, PauseOutlined, SoundOutlined } from '@ant-design/icons'
|
import { ClearOutlined, CopyOutlined, FolderOpenOutlined, PauseOutlined, SearchOutlined, SelectOutlined, SnippetsOutlined, SoundOutlined } from '@ant-design/icons'
|
||||||
import { getThemeById } from '@shared/theme'
|
import { getThemeById } from '@shared/theme'
|
||||||
import type { CommandItem } from '@shared/commands'
|
import type { CommandItem } from '@shared/commands'
|
||||||
import type { PtyDataEvent, PtyExitEvent } from '@shared/ipc'
|
import type { PtyDataEvent, PtyExitEvent } from '@shared/ipc'
|
||||||
@@ -280,6 +280,10 @@ export const TerminalView: ForwardRefExoticComponent<TerminalViewProps & { ref?:
|
|||||||
const [searchOpen, setSearchOpen] = useState(false)
|
const [searchOpen, setSearchOpen] = useState(false)
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [resultInfo, setResultInfo] = useState('')
|
const [resultInfo, setResultInfo] = useState('')
|
||||||
|
// Right-click context menu: position + whether a selection existed at open
|
||||||
|
// time (decides if 复制 is enabled).
|
||||||
|
const [menuPos, setMenuPos] = useState<{ x: number; y: number } | null>(null)
|
||||||
|
const [menuHasSel, setMenuHasSel] = useState(false)
|
||||||
const [paste, setPaste] = useState<string | null>(null)
|
const [paste, setPaste] = useState<string | null>(null)
|
||||||
const [dead, setDead] = useState(false)
|
const [dead, setDead] = useState(false)
|
||||||
const [exitCode, setExitCode] = useState(0)
|
const [exitCode, setExitCode] = useState(0)
|
||||||
@@ -339,6 +343,40 @@ export const TerminalView: ForwardRefExoticComponent<TerminalViewProps & { ref?:
|
|||||||
})
|
})
|
||||||
}, [sessionId])
|
}, [sessionId])
|
||||||
|
|
||||||
|
const openSearch = useCallback(() => {
|
||||||
|
searchOpenRef.current = true
|
||||||
|
setSearchOpen(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const openContextMenu = useCallback((e: React.MouseEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setMenuHasSel(Boolean(termRef.current?.getSelection()))
|
||||||
|
// Clamp so the menu never overflows the window edge.
|
||||||
|
const x = Math.min(e.clientX, window.innerWidth - 216)
|
||||||
|
const y = Math.min(e.clientY, window.innerHeight - 200)
|
||||||
|
setMenuPos({ x: Math.max(x, 4), y: Math.max(y, 4) })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Global dismiss: Escape / scroll / window blur close the context menu.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!menuPos) return
|
||||||
|
const close = (): void => setMenuPos(null)
|
||||||
|
const onKey = (e: KeyboardEvent): void => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.stopPropagation()
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('keydown', onKey, true)
|
||||||
|
window.addEventListener('wheel', close, true)
|
||||||
|
window.addEventListener('blur', close)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', onKey, true)
|
||||||
|
window.removeEventListener('wheel', close, true)
|
||||||
|
window.removeEventListener('blur', close)
|
||||||
|
}
|
||||||
|
}, [menuPos])
|
||||||
|
|
||||||
const closeSearch = useCallback(() => {
|
const closeSearch = useCallback(() => {
|
||||||
searchOpenRef.current = false
|
searchOpenRef.current = false
|
||||||
searchRef.current?.clearDecorations()
|
searchRef.current?.clearDecorations()
|
||||||
@@ -470,8 +508,7 @@ export const TerminalView: ForwardRefExoticComponent<TerminalViewProps & { ref?:
|
|||||||
}
|
}
|
||||||
const mod = navigator.platform.toLowerCase().includes('mac') ? e.metaKey : e.ctrlKey
|
const mod = navigator.platform.toLowerCase().includes('mac') ? e.metaKey : e.ctrlKey
|
||||||
if ((e.key === 'f' || e.key === 'F') && mod && !e.altKey) {
|
if ((e.key === 'f' || e.key === 'F') && mod && !e.altKey) {
|
||||||
searchOpenRef.current = true
|
openSearch()
|
||||||
setSearchOpen(true)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (isModShiftChord(e, 'c')) {
|
if (isModShiftChord(e, 'c')) {
|
||||||
@@ -521,7 +558,7 @@ export const TerminalView: ForwardRefExoticComponent<TerminalViewProps & { ref?:
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
[closeSearch, copySelection, pasteFromClipboard, moveSelection, handleTab]
|
[closeSearch, openSearch, copySelection, pasteFromClipboard, moveSelection, handleTab]
|
||||||
)
|
)
|
||||||
|
|
||||||
// ---- terminal instance: built once per session ----
|
// ---- terminal instance: built once per session ----
|
||||||
@@ -834,8 +871,73 @@ export const TerminalView: ForwardRefExoticComponent<TerminalViewProps & { ref?:
|
|||||||
searchTermRef.current = value
|
searchTermRef.current = value
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const runMenuAction = useCallback(
|
||||||
|
(action: () => void) => {
|
||||||
|
setMenuPos(null)
|
||||||
|
action()
|
||||||
|
termRef.current?.focus()
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={terminalClassName} onMouseDown={() => termRef.current?.focus()}>
|
<div className={terminalClassName} onMouseDown={() => termRef.current?.focus()} onContextMenu={openContextMenu}>
|
||||||
|
{menuPos && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="term-menu-overlay"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
setMenuPos(null)
|
||||||
|
termRef.current?.focus()
|
||||||
|
}}
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
setMenuPos(null)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="term-menu" style={{ left: menuPos.x, top: menuPos.y }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="term-menu-item"
|
||||||
|
disabled={!menuHasSel}
|
||||||
|
onClick={() => runMenuAction(() => void copySelection())}
|
||||||
|
>
|
||||||
|
<CopyOutlined />
|
||||||
|
<span className="term-menu-label">复制</span>
|
||||||
|
<kbd>Ctrl+Shift+C</kbd>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="term-menu-item"
|
||||||
|
disabled={dead}
|
||||||
|
onClick={() => runMenuAction(() => void pasteFromClipboard())}
|
||||||
|
>
|
||||||
|
<SnippetsOutlined />
|
||||||
|
<span className="term-menu-label">粘贴</span>
|
||||||
|
<kbd>Ctrl+Shift+V</kbd>
|
||||||
|
</button>
|
||||||
|
<div className="term-menu-sep" />
|
||||||
|
<button type="button" className="term-menu-item" onClick={() => runMenuAction(openSearch)}>
|
||||||
|
<SearchOutlined />
|
||||||
|
<span className="term-menu-label">查找</span>
|
||||||
|
<kbd>Ctrl+F</kbd>
|
||||||
|
</button>
|
||||||
|
<button type="button" className="term-menu-item" onClick={() => runMenuAction(() => termRef.current?.selectAll())}>
|
||||||
|
<SelectOutlined />
|
||||||
|
<span className="term-menu-label">全选</span>
|
||||||
|
<kbd />
|
||||||
|
</button>
|
||||||
|
<button type="button" className="term-menu-item" onClick={() => runMenuAction(() => termRef.current?.clear())}>
|
||||||
|
<ClearOutlined />
|
||||||
|
<span className="term-menu-label">清屏</span>
|
||||||
|
<kbd />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{searchOpen && (
|
{searchOpen && (
|
||||||
<SearchBar
|
<SearchBar
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
|
|||||||
@@ -118,6 +118,73 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- right-click context menu ---- */
|
||||||
|
.term-menu-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-menu {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 41;
|
||||||
|
min-width: 200px;
|
||||||
|
padding: 4px;
|
||||||
|
border: 1px solid var(--chrome-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--chrome-bg-deep);
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--chrome-fg);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--chrome-fg);
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-menu-item:hover:not(:disabled),
|
||||||
|
.term-menu-item:focus-visible {
|
||||||
|
background: var(--chrome-hover);
|
||||||
|
color: var(--chrome-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-menu-item:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-menu-label {
|
||||||
|
flex: 1;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-menu-item > kbd {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--chrome-fg);
|
||||||
|
opacity: 0.55;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.term-menu-sep {
|
||||||
|
height: 1px;
|
||||||
|
margin: 4px 8px;
|
||||||
|
background: var(--chrome-border);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- session log record / open-dir buttons (floating, top-left) ---- */
|
/* ---- session log record / open-dir buttons (floating, top-left) ---- */
|
||||||
.term-recbar {
|
.term-recbar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -107,5 +107,5 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
|||||||
},
|
},
|
||||||
customThemes: [],
|
customThemes: [],
|
||||||
highlightRules: DEFAULT_HIGHLIGHT_RULES,
|
highlightRules: DEFAULT_HIGHLIGHT_RULES,
|
||||||
system: { launchAtLogin: false, preventSleep: false, globalShowHide: '', closeAction: 'ask', autoCheckUpdate: true }
|
system: { launchAtLogin: false, preventSleep: false, globalShowHide: '', closeAction: 'tray', autoCheckUpdate: true }
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user