Compare commits

...
2 Commits
Author SHA1 Message Date
Bill 33efbe921e chore: release v1.0.18
CI / typecheck + test + build (windows) (push) Canceled after 0s
2026-09-24 22:53:29 +08:00
Bill 83dc3f15cc fix(lock): lock screen swallowed every keystroke; add Ctrl+L to lock
The renderer-side guard added in v1.0.17 called preventDefault on every
keydown while locked. A keydown's default action IS inserting the
character into the focused field, so the lock screen's password box
received nothing and a locked app could never be unlocked. Menu
accelerators are already stopped in main (before-input-event); the
renderer guard now just skips its own logic.

Also: Ctrl+L locks the screen from anywhere in the app, terminals
included. The chord is only taken when a lock actually engages, so an
unconfigured app keeps Ctrl+L for the shell's clear-screen.
2026-09-24 22:53:27 +08:00
12 changed files with 60 additions and 16 deletions

No files matched your search

+2 -1
View File
@@ -62,7 +62,8 @@ Electron + electron-vite + React 终端工具(本地终端 / SSH / SFTP)。
- 冷却阶梯 1s→2s→5s→10s→30s,失败计数与冷却同样落盘;`setPassword`/`clearPassword`/`unlock` 走内部串行队列(`serialize`),否则并发调用会同时通过闸门绕过冷却
- 闲置锁屏:`powerMonitor.getSystemIdleTime()`,15s 轮询;读不到(无会话/工作站已锁)一律当「不闲置」。`settings.lock.autoLockMinutes` 是白名单 `{0,1,5,15,30,60}`(`src/shared/settings.ts` 的 `LOCK_AUTO_DELAYS`),0 = 从不
- **清除密码会一并把 `settings.lock.enabled`/`lockAtStartup` 置 false**(`LockControllerOptions.clearLockPreferences`,默认走 `mutateSettings`):设置页文案承诺「清除后锁屏会一并关闭」,留着会让用户下次设密码时被静默重新武装
- 锁屏期间主进程在 `win.webContents.on('before-input-event')` 里吞掉 F5/Ctrl+R、Ctrl+±0(含 Shift 拼写)、Ctrl+Shift+I/J/C:遮罩是 DOM 层,拦不住浏览器进程处理的 Electron 默认菜单加速键,而重载会触发 `beforeunload` 把遮罩后面的会话全杀掉。渲染层另有一道 `document.documentElement.dataset.locked` 守卫(字体快捷键、`Ctrl+PgUp/PgDn`)
- 锁屏期间主进程在 `win.webContents.on('before-input-event')` 里吞掉 F5/Ctrl+R、Ctrl+±0(含 Shift 拼写)、Ctrl+Shift+I/J/C:遮罩是 DOM 层,拦不住浏览器进程处理的 Electron 默认菜单加速键,而重载会触发 `beforeunload` 把遮罩后面的会话全杀掉。渲染层的 `document.documentElement.dataset.locked` 守卫(字体快捷键、`Ctrl+PgUp/PgDn`)**只允许 `return` 跳过自身逻辑,绝不能 `preventDefault`**——keydown 的默认动作就是「往聚焦输入框插字符」,窗口级 preventDefault 会把锁屏密码框的全部输入杀掉(v1.0.17 就是这么坏的,v1.0.18 修复)
- **Ctrl+L = 立即锁屏**(同一 `before-input-event` 里捕获,终端里也生效——这正是它的意义):仅在锁定真的生效时才 `preventDefault`,未设置密码的应用保留 Ctrl+L 给 shell 的清屏;已锁定时不再拦截
- 启动时**不要**用 `locked: true` 作渲染层初值再直接画锁屏:`App.tsx` 用 `null` 表示「主进程还没答复」,此时只画 `.lock-screen-boot` 纯色层,否则每次启动都会给没设密码的用户闪一帧锁屏。`getLockState()` 失败时要落到「locked 且未配置」的状态,让输入框可达(主进程对无 verifier 的解锁请求直接放行)
- 相关测试:`node tests/lock-store.mjs`(verifier + 状态存储)、`node tests/lock-controller.mjs`(冷却阶梯、并发串行化、落盘恢复、闲置触发、清除联动)
+7
View File
@@ -1,5 +1,12 @@
# OpenTerminal Changelog
## v1.0.18 - 2026-09-24
- **Fixed a serious v1.0.17 regression: the password could not be typed on the lock screen**. v1.0.17 swallowed all keyboard input at the renderer level while locked (meant to block zoom/reload shortcuts), but the default action of a key press is exactly "insert the character into the focused field" — so the lock screen's password box received nothing and the app could not be unlocked.
- Shortcut blocking now happens entirely in the main process (`before-input-event`) and only for reload / DevTools / zoom chords; normal typing is unaffected.
- **New lock shortcut Ctrl+L**: press it anywhere inside the app — terminals included — to lock immediately. When no lock password is set the chord is left alone (a shell's Ctrl+L clear-screen keeps working).
- **If v1.0.17 locked you out**: quit the app completely → delete `%APPDATA%\OpenTerminal\lock.json` → start it again (with the lock data gone the screen will not lock), then upgrade and set a new lock password.
## v1.0.17 - 2026-09-24
- **New Settings → Lock tab**: set, change or remove a lock password (stored on this machine only, scrypt one-way hash — no plaintext, no account system)
+7
View File
@@ -1,5 +1,12 @@
# OpenTerminal 更新履歴
## v1.0.18 - 2026-09-24
- **v1.0.17 の重大なリグレッションを修正:ロック画面でパスワードを入力できない**。v1.0.17 ではロック中、レンダラー側ですべてのキーボード入力を握りつぶしていました(ズーム/再読み込みのショートカットを無効化する意図でした)。しかしキー入力の既定動作は「フォーカスされたフィールドへの文字挿入」であり、その結果、ロック画面のパスワード欄に何も入力できず、ロック解除もできなくなっていました。
- ショートカットの無効化はすべてメインプロセス(`before-input-event`)に移し、再読み込み/開発者ツール/ズームの組み合わせだけを対象にしました。通常の入力には影響しません。
- **ロック用ショートカット Ctrl+L を新設**:アプリ内のどこでも(ターミナル内を含む)押すとすぐロックします。ロックパスワード未設定のときはこの組み合わせを奪いません(shell の Ctrl+L クリアはそのまま使えます)。
- **v1.0.17 でロックされたままの方の復旧手順**:アプリを完全に終了 → `%APPDATA%\OpenTerminal\lock.json` を削除 → 再起動(ロックデータがなければロック画面は出ません)。その後、本バージョンに更新してロックパスワードを再設定してください。
## v1.0.17 - 2026-09-24
- **設定 → ロック を新設**:ロック画面のパスワードを設定・変更・削除できます(このマシンにのみ保存、scrypt 一方向ハッシュ。平文なし・アカウント体系なし)
+7
View File
@@ -1,5 +1,12 @@
# OpenTerminal 更新日志
## v1.0.18 - 2026-09-24
- **修复 v1.0.17 的严重回归:锁屏后无法输入密码**。v1.0.17 在锁屏期间由渲染层拦截了全部键盘输入(本意是挡住缩放/刷新快捷键),但按键的默认行为正是「向聚焦的输入框插入字符」,导致锁屏界面密码框收不到任何输入、无法解锁。
- 快捷键拦截已全部移到主进程(`before-input-event`),只拦截刷新/开发者工具/缩放组合键,普通输入不受影响。
- **新增锁屏快捷键 Ctrl+L**:应用内任意位置(包括终端里)按下立即锁屏;未设置锁屏密码时不占用该组合键(shell 的 Ctrl+L 清屏不受影响)。
- **已被 v1.0.17 锁定的用户恢复方法**:完全退出应用 → 删除 `%APPDATA%\OpenTerminal\lock.json` → 重新启动(锁屏数据删除后不会再弹锁屏),升级到本版本后可重新设置锁屏密码。
## v1.0.17 - 2026-09-24
- **新增 设置 → 锁屏 选项卡**:可设置、修改、移除锁屏密码(仅存本机,scrypt 单向哈希,不存明文、无账号体系)
+7
View File
@@ -1,5 +1,12 @@
# OpenTerminal 更新日誌
## v1.0.18 - 2026-09-24
- **修復 v1.0.17 的嚴重回歸:鎖定後無法輸入密碼**。v1.0.17 在鎖定期間由繪製程序攔截了全部鍵盤輸入(本意是擋住縮放/重新整理快捷鍵),但按鍵的預設行為正是「向聚焦的輸入框插入字元」,導致鎖定畫面密碼框收不到任何輸入、無法解鎖。
- 快捷鍵攔截已全部移到主程序(`before-input-event`),只攔截重新整理/開發者工具/縮放組合鍵,一般輸入不受影響。
- **新增鎖定快捷鍵 Ctrl+L**:應用內任意位置(包括終端機裡)按下立即鎖定;未設定鎖定密碼時不占用該組合鍵(shell 的 Ctrl+L 清畫面不受影響)。
- **已被 v1.0.17 鎖定的使用者恢復方法**:完全結束應用 → 刪除 `%APPDATA%\OpenTerminal\lock.json` → 重新啟動(鎖定資料刪除後不會再彈鎖定畫面),升級到本版本後可重新設定鎖定密碼。
## v1.0.17 - 2026-09-24
- **新增 設定 → 鎖定 分頁**:可設定、修改、移除鎖定畫面密碼(僅存於本機、scrypt 單向雜湊,不存明文、無帳號體系)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "open-terminal",
"productName": "OpenTerminal",
"version": "1.0.17",
"version": "1.0.18",
"description": "Open-source terminal with SSH, split panes, themes and fonts",
"main": "out/main/index.js",
"author": "CodingPlan.Site",
+18 -1
View File
@@ -166,12 +166,29 @@ function createWindow(): void {
}
})
// Ctrl+L is the panic lock: it must work from anywhere in the app, terminals
// included, so it is captured here ahead of the page. It only takes the chord
// away when a lock actually engages — an unconfigured app keeps Ctrl+L for
// the shell's clear-screen.
// Swallow the menu accelerators that would otherwise act behind the lock
// overlay (see isLockBlockedShortcut). A throw in here would break typing
// altogether, so the whole guard is defensive.
win.webContents.on('before-input-event', (event, input) => {
try {
if (input.type !== 'keyDown' || !getLockController().isLocked()) return
if (input.type !== 'keyDown') return
const lock = getLockController()
if (
!lock.isLocked() &&
input.control &&
!input.shift &&
!input.alt &&
!input.meta &&
input.key.toLowerCase() === 'l'
) {
if (lock.lockNow().locked) event.preventDefault()
return
}
if (!lock.isLocked()) return
if (isLockBlockedShortcut(input)) event.preventDefault()
} catch {
/* an input guard must never take the window down with it */
+7 -9
View File
@@ -157,15 +157,13 @@ function FontHotkeyListener(): null {
useEffect(() => {
const onKeyDown = (e: KeyboardEvent): void => {
// The lock overlay leaves Workspace mounted; window-capture hotkeys must
// not mutate font size on a shell hidden behind it. preventDefault matters
// here: returning alone lets the chord reach Electron's default menu
// accelerators (zoomIn/zoomOut/resetZoom), which rescale the whole UI
// behind the opaque mask and make Chromium persist that zoom per origin.
if (document.documentElement.dataset.locked === 'true') {
e.preventDefault()
return
}
// While locked, skip only this listener's own logic — and NEVER call
// preventDefault here. The default action of a keydown is inserting the
// character into the focused field, and it runs after the whole dispatch:
// preventing it at window level kills every keystroke the lock screen's
// password field would receive (shipped broken in v1.0.17). The menu
// accelerators are stopped in main, on before-input-event, instead.
if (document.documentElement.dataset.locked === 'true') return
if (!e.ctrlKey || e.metaKey) return
const target = e.target as HTMLElement | null
const isXtermHelper =
+1 -1
View File
@@ -134,7 +134,7 @@ const settings: Record<string, string> = {
'settings.lock.lockAtStartup': 'Lock at startup',
'settings.lock.lockAtStartupDesc': 'Ask for the password right after every launch',
'settings.lock.lockNow': 'Lock now',
'settings.lock.lockNowDesc': 'Lock the main window immediately (sessions keep running)',
'settings.lock.lockNowDesc': 'Lock the main window immediately (sessions keep running, Ctrl+L)',
'settings.lock.title': 'Locked',
'settings.lock.screenDesc': 'Enter the lock password to unlock',
'settings.lock.passwordPlaceholder': 'Password',
+1 -1
View File
@@ -131,7 +131,7 @@ const settings: Record<string, string> = {
'settings.lock.lockAtStartup': '起動時にロック',
'settings.lock.lockAtStartupDesc': '起動直後にパスワードの入力を求めます',
'settings.lock.lockNow': '今すぐロック',
'settings.lock.lockNowDesc': 'メインウィンドウをすぐにロックします(セッションは動作を続けます)',
'settings.lock.lockNowDesc': 'メインウィンドウをすぐにロックします(セッションは動作を続けます、Ctrl+L)',
'settings.lock.title': 'ロック中',
'settings.lock.screenDesc': 'ロックパスワードを入力して解除します',
'settings.lock.passwordPlaceholder': 'パスワード',
+1 -1
View File
@@ -126,7 +126,7 @@ const settings: Record<string, string> = {
'settings.lock.lockAtStartup': '启动时锁屏',
'settings.lock.lockAtStartupDesc': '每次启动后先要求输入密码',
'settings.lock.lockNow': '立即锁屏',
'settings.lock.lockNowDesc': '马上锁定主窗口(会话保持运行)',
'settings.lock.lockNowDesc': '马上锁定主窗口(会话保持运行,快捷键 Ctrl+L)',
'settings.lock.title': '已锁定',
'settings.lock.screenDesc': '输入锁屏密码解锁',
'settings.lock.passwordPlaceholder': '密码',
+1 -1
View File
@@ -126,7 +126,7 @@ const settings: Record<string, string> = {
'settings.lock.lockAtStartup': '啟動時鎖定',
'settings.lock.lockAtStartupDesc': '每次啟動後先要求輸入密碼',
'settings.lock.lockNow': '立即鎖定',
'settings.lock.lockNowDesc': '馬上鎖定主視窗(工作階段保持運作)',
'settings.lock.lockNowDesc': '馬上鎖定主視窗(工作階段保持運作,快捷鍵 Ctrl+L)',
'settings.lock.title': '已鎖定',
'settings.lock.screenDesc': '輸入鎖定密碼以解鎖',
'settings.lock.passwordPlaceholder': '密碼',