Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97fc7171cc | ||
|
|
d3d029fe4f | ||
|
|
77fd241b43 |
No files matched your search
@@ -5,6 +5,7 @@ Electron + electron-vite + React 终端工具(本地终端 / SSH / SFTP)。
|
|||||||
## 常用命令
|
## 常用命令
|
||||||
|
|
||||||
- 开发:`npm run dev`(主进程改动不热重建,需重启)
|
- 开发:`npm run dev`(主进程改动不热重建,需重启)
|
||||||
|
- dev 实例使用独立用户数据目录 `%APPDATA%\OpenTerminal-dev` 与独立单实例锁(`src/main/index.ts` 顶部 `!app.isPackaged` 分支),窗口标题带 `(dev)`:**可与已安装的正式版同时运行,互不干扰**,也不会把测试设置/会话写进真实配置
|
||||||
- 类型检查:`npx tsc --noEmit -p tsconfig.web.json`
|
- 类型检查:`npx tsc --noEmit -p tsconfig.web.json`
|
||||||
- 打包:`npm run dist`,产物在 `release/`(msi + exe + latest.yml + blockmap)
|
- 打包:`npm run dist`,产物在 `release/`(msi + exe + latest.yml + blockmap)
|
||||||
- 国内网络需镜像:`ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ npm run dist`
|
- 国内网络需镜像:`ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ npm run dist`
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "open-terminal",
|
"name": "open-terminal",
|
||||||
"productName": "OpenTerminal",
|
"productName": "OpenTerminal",
|
||||||
"version": "1.0.9",
|
"version": "1.0.10",
|
||||||
"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",
|
||||||
|
|||||||
@@ -20,6 +20,15 @@ function showOrCreate(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dev runs get their own userData directory (settings, session snapshot,
|
||||||
|
// command history, logs) *and* their own single-instance lock — both are keyed
|
||||||
|
// on that path. Without this a dev instance fights the installed build: it
|
||||||
|
// takes the lock, so the other side is refused / must be killed, and it writes
|
||||||
|
// test data straight into the real profile. Must run before the lock below.
|
||||||
|
if (!app.isPackaged) {
|
||||||
|
app.setPath('userData', join(app.getPath('appData'), 'OpenTerminal-dev'))
|
||||||
|
}
|
||||||
|
|
||||||
// Single instance: a second launch just surfaces the existing window (pulls
|
// Single instance: a second launch just surfaces the existing window (pulls
|
||||||
// it out of the tray if hidden there) instead of starting another process.
|
// it out of the tray if hidden there) instead of starting another process.
|
||||||
const gotSingleInstanceLock = app.requestSingleInstanceLock()
|
const gotSingleInstanceLock = app.requestSingleInstanceLock()
|
||||||
@@ -64,6 +73,15 @@ function createWindow(): void {
|
|||||||
|
|
||||||
win.on('ready-to-show', () => win.show())
|
win.on('ready-to-show', () => win.show())
|
||||||
|
|
||||||
|
// Dev window wears a "(dev)" tag so it is never confused with the installed
|
||||||
|
// build sitting next to it (they no longer share userData — see above).
|
||||||
|
if (!app.isPackaged) {
|
||||||
|
win.on('page-title-updated', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
win.setTitle('OpenTerminal (dev)')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Close button → tray / exit per the closeAction setting (ask by default).
|
// Close button → tray / exit per the closeAction setting (ask by default).
|
||||||
win.on('close', (e) => {
|
win.on('close', (e) => {
|
||||||
void onMainWindowClose(win, e, showOrCreate)
|
void onMainWindowClose(win, e, showOrCreate)
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ export function RenderSettingsTab(): React.JSX.Element {
|
|||||||
/>
|
/>
|
||||||
<SettingRow
|
<SettingRow
|
||||||
label="粘贴风险确认"
|
label="粘贴风险确认"
|
||||||
desc="粘贴前弹出风险确认提示"
|
desc="多行文本(或超长单行)粘贴前弹出确认;单行直接粘贴"
|
||||||
control={
|
control={
|
||||||
<Switch
|
<Switch
|
||||||
checked={settings.terminal.pasteRiskConfirm}
|
checked={settings.terminal.pasteRiskConfirm}
|
||||||
|
|||||||
@@ -32,10 +32,18 @@ function toFontWeight(n: number): 'normal' | 'bold' | number {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Paste-risk heuristic: multi-line or unreasonably long text. */
|
/**
|
||||||
function isRiskyPaste(text: string): boolean {
|
* Paste-risk heuristic, split by shape rather than by raw length:
|
||||||
if (text.includes('\n') || text.includes('\r')) return true
|
* - two or more lines always confirm — a stray newline executes a command
|
||||||
return text.length > 100
|
* the user never reviewed;
|
||||||
|
* - a single line pastes straight through, unless it is unusually long
|
||||||
|
* (those are pasted scripts rather than something typed by hand).
|
||||||
|
*/
|
||||||
|
const SINGLE_LINE_CONFIRM_LENGTH = 1000
|
||||||
|
function needsPasteConfirm(text: string): boolean {
|
||||||
|
const normalized = text.replace(/\r\n?/g, '\n').replace(/\n$/, '')
|
||||||
|
if (normalized.includes('\n')) return true
|
||||||
|
return normalized.length > SINGLE_LINE_CONFIRM_LENGTH
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -511,19 +519,17 @@ export const TerminalView: ForwardRefExoticComponent<TerminalViewProps & { ref?:
|
|||||||
|
|
||||||
const beginPaste = useCallback(
|
const beginPaste = useCallback(
|
||||||
(text: string) => {
|
(text: string) => {
|
||||||
if (
|
// Read the setting at call time: this runs from a long-lived keydown
|
||||||
!deadRef.current &&
|
// listener, so a captured value would go stale after a settings change.
|
||||||
settings.terminal.pasteRiskConfirm &&
|
const confirmOn = useSettingsStore.getState().settings.terminal.pasteRiskConfirm
|
||||||
!pasteConfirmedForSession &&
|
if (!deadRef.current && confirmOn && !pasteConfirmedForSession && needsPasteConfirm(text)) {
|
||||||
isRiskyPaste(text)
|
|
||||||
) {
|
|
||||||
nativePasteRef.current = text
|
nativePasteRef.current = text
|
||||||
setPaste({ noPrompt: false, disableDetection: false })
|
setPaste({ noPrompt: false, disableDetection: false })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
confirmPaste(text)
|
confirmPaste(text)
|
||||||
},
|
},
|
||||||
[settings.terminal.pasteRiskConfirm, confirmPaste]
|
[confirmPaste]
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Confirm the risky-paste dialog: apply its choices, then paste. */
|
/** Confirm the risky-paste dialog: apply its choices, then paste. */
|
||||||
@@ -873,9 +879,36 @@ export const TerminalView: ForwardRefExoticComponent<TerminalViewProps & { ref?:
|
|||||||
observerRef.current = observer
|
observerRef.current = observer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Plain Ctrl/Cmd+V: not a chord xterm handles, so it used to reach the pty
|
||||||
|
// as a literal ^V (0x16) — nothing pasted. Handled here, on the terminal
|
||||||
|
// host in the capture phase, with preventDefault so the pty never sees it
|
||||||
|
// and Chromium's own paste cannot double up. Text follows the same
|
||||||
|
// single-line / multi-line rule as the Ctrl+Shift+V path.
|
||||||
|
const host = hostRef.current
|
||||||
|
const onPasteKey = (e: KeyboardEvent): void => {
|
||||||
|
if (e.type !== 'keydown' || e.altKey) return
|
||||||
|
const mod = navigator.platform.toLowerCase().includes('mac') ? e.metaKey : e.ctrlKey
|
||||||
|
if (!mod || e.shiftKey || e.key.toLowerCase() !== 'v') return
|
||||||
|
const target = e.target as HTMLElement | null
|
||||||
|
const isXtermHelper =
|
||||||
|
target instanceof HTMLTextAreaElement && target.classList.contains('xterm-helper-textarea')
|
||||||
|
if (
|
||||||
|
target &&
|
||||||
|
!isXtermHelper &&
|
||||||
|
(target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)
|
||||||
|
) {
|
||||||
|
return // ordinary inputs keep the browser's native paste
|
||||||
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
void pasteFromClipboard()
|
||||||
|
}
|
||||||
|
host?.addEventListener('keydown', onPasteKey, true)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (carryTimer !== undefined) window.clearTimeout(carryTimer)
|
if (carryTimer !== undefined) window.clearTimeout(carryTimer)
|
||||||
if (ptyTimerRef.current) window.clearTimeout(ptyTimerRef.current)
|
if (ptyTimerRef.current) window.clearTimeout(ptyTimerRef.current)
|
||||||
|
host?.removeEventListener('keydown', onPasteKey, true)
|
||||||
for (const unsubscribe of unsubscribes) unsubscribe()
|
for (const unsubscribe of unsubscribes) unsubscribe()
|
||||||
for (const disposable of disposables) disposable.dispose()
|
for (const disposable of disposables) disposable.dispose()
|
||||||
observer?.disconnect()
|
observer?.disconnect()
|
||||||
|
|||||||
Reference in new issue
Block a user