From a65aec48dc38f8795b942116ef52eeaedddf7aa5 Mon Sep 17 00:00:00 2001 From: senlinjun <2755221817@qq.com> Date: Thu, 13 Aug 2026 19:48:28 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=BA=86?= =?UTF-8?q?=E5=AF=B9=20Linux=20=E7=9A=84=E6=94=AF=E6=8C=81=E4=BB=A5?= =?UTF-8?q?=E5=8F=8A=20pacman=20=E7=9A=84=E6=89=93=E5=8C=85=E6=96=B9?= =?UTF-8?q?=E5=BC=8F=20docs:=20=E6=B7=BB=E5=8A=A0=E6=9C=89=E5=85=B3?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E8=A1=8C=E5=90=AF=E5=8A=A8=E7=9A=84=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 24 ++ README_EN.md | 24 ++ build/deb-after-install.sh | 57 +++ build/deb-after-remove.sh | 28 ++ build/icons/128x128.png | Bin 0 -> 11179 bytes build/icons/16x16.png | Bin 0 -> 940 bytes build/icons/24x24.png | Bin 0 -> 1630 bytes build/icons/256x256.png | Bin 0 -> 23758 bytes build/icons/32x32.png | Bin 0 -> 2423 bytes build/icons/48x48.png | Bin 0 -> 3729 bytes build/icons/512x512.png | Bin 0 -> 52523 bytes build/icons/64x64.png | Bin 0 -> 5190 bytes build/pacman-after-install.sh | 21 + build/pacman-after-remove.sh | 20 + build/ztools-launcher.sh | 25 ++ changelog.md | 4 + docs/examples/provider-example/README.md | 8 +- docs/provider-development-guide.md | 10 +- electron-builder.yml | 18 + internal-plugins/setting/src/env.d.ts | 2 + .../views/GeneralSetting/GeneralSetting.vue | 60 ++- package.json | 4 + resources/floatingBall.html | 30 +- resources/preload.js | 8 + scripts/ztools-cli.mjs | 84 ++++ src/main/api/plugin/internal.ts | 25 ++ src/main/api/renderer/settings.ts | 21 + src/main/api/renderer/window.ts | 10 + src/main/appMain.ts | 8 + src/main/core/cliWake.ts | 9 + src/main/core/floatingBallDragMode.ts | 18 + src/main/core/floatingBallManager.ts | 147 ++++++- src/main/core/linuxAutoStart.ts | 91 +++++ src/main/core/shortcutFallback.ts | 371 ++++++++++++++++++ src/main/managers/windowManager.ts | 233 ++++++++++- src/preload/index.ts | 12 + .../src/components/search/SearchBox.vue | 29 ++ src/renderer/src/env.d.ts | 1 + tests/e2e/ztools.spec.ts | 217 +++++++++- tests/main/cliWake.test.ts | 24 ++ tests/main/floatingBallDragMode.test.ts | 19 + tests/main/linuxAutoStart.test.ts | 109 +++++ tests/main/pluginPreloadInternalApi.test.ts | 14 + .../settingsOptimizedShortcutFallback.test.ts | 62 ++- 44 files changed, 1807 insertions(+), 40 deletions(-) create mode 100755 build/deb-after-install.sh create mode 100755 build/deb-after-remove.sh create mode 100644 build/icons/128x128.png create mode 100644 build/icons/16x16.png create mode 100644 build/icons/24x24.png create mode 100644 build/icons/256x256.png create mode 100644 build/icons/32x32.png create mode 100644 build/icons/48x48.png create mode 100644 build/icons/512x512.png create mode 100644 build/icons/64x64.png create mode 100755 build/pacman-after-install.sh create mode 100755 build/pacman-after-remove.sh create mode 100755 build/ztools-launcher.sh create mode 100755 scripts/ztools-cli.mjs create mode 100644 src/main/core/cliWake.ts create mode 100644 src/main/core/floatingBallDragMode.ts create mode 100644 src/main/core/linuxAutoStart.ts create mode 100644 src/main/core/shortcutFallback.ts create mode 100644 tests/main/cliWake.test.ts create mode 100644 tests/main/floatingBallDragMode.test.ts create mode 100644 tests/main/linuxAutoStart.test.ts diff --git a/README.md b/README.md index d833ddf4..57e6ed9b 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,27 @@ pnpm build:linux:arm64 # Linux (按 arm64 架构打包) 2. 输入应用名称或命令进行搜索 3. 按 `↑` `↓` `←` `→` 选择,`Enter` 确认,`Esc` 退出 +### 命令行唤起 + +在终端执行 `ztools` 可以唤起已运行的 ZTools 实例并显示主窗口;如果应用尚未运行,则会先启动应用再显示窗口。命令会立即返回,不阻塞终端。 + +```bash +# Linux(deb 安装包)与 macOS 手动安装后可直接使用 +ztools + +# 开发环境中运行(先执行 pnpm build 构建主程序) +pnpm ztools + +# 通过环境变量指定可执行文件 +ZTOOLS_BIN=/path/to/ZTools ztools +``` + +不同安装方式下的可用性: + +- **Linux deb**:安装包会自动创建 `/usr/bin/ztools` 命令,卸载时自动移除 +- **macOS dmg**:首次使用需手动建立软链接:`sudo ln -s "/Applications/ZTools.app/Contents/Resources/ztools/ztools-launcher.sh" /usr/local/bin/ztools` +- **Windows**:仅覆盖开发环境(`pnpm ztools`) + ## 🧩 插件开发 ZTools 是一个强大、可扩展的插件平台,使用自定义插件提升您的生产力。通过简单的配置、丰富的 API 以及跨平台支持,您可以轻松开发出功能强大的插件。 @@ -222,6 +243,9 @@ pnpm build:linux # 打包 Linux 应用 (当前主机架构) pnpm build:linux:x64 # 打包 Linux 应用 (amd64/x64) pnpm build:linux:arm64 # 打包 Linux 应用 (arm64) pnpm build:unpack # 打包但不生成安装包(调试用) + +# 命令行唤起 +pnpm ztools # 唤起已运行的实例(或启动应用) ``` ### 调试 diff --git a/README_EN.md b/README_EN.md index da4fa3ed..19ba6d4c 100644 --- a/README_EN.md +++ b/README_EN.md @@ -124,6 +124,27 @@ pnpm build:linux:arm64 # Linux (arm64) 2. Enter application name or command to search 3. Use `↑` `↓` `←` `→` to navigate, `Enter` to confirm, `Esc` to exit +### Wake Up from Command Line + +Run `ztools` in a terminal to bring up an already running ZTools instance and show its main window; if the app is not running yet, it will be started first and then shown. The command returns immediately and does not block the terminal. + +```bash +# Linux (deb package) and macOS (after manual install) can use it directly +ztools + +# In the development environment (run pnpm build first) +pnpm ztools + +# Point to a specific executable with an environment variable +ZTOOLS_BIN=/path/to/ZTools ztools +``` + +Availability by installation method: + +- **Linux deb**: the package creates `/usr/bin/ztools` automatically and removes it on uninstall +- **macOS dmg**: create a symlink manually on first use: `sudo ln -s "/Applications/ZTools.app/Contents/Resources/ztools/ztools-launcher.sh" /usr/local/bin/ztools` +- **Windows**: development environment only (`pnpm ztools`) + ## 🧩 Plugin Development ZTools is a powerful and extensible plugin platform that enhances your productivity with custom plugins. With simple configuration, rich APIs, and cross-platform support, you can easily develop powerful plugins. @@ -210,6 +231,9 @@ pnpm build:linux # Package Linux app (Default Arch) pnpm build:linux:x64 # Package Linux app (amd64/x64) pnpm build:linux:arm64 # Package Linux app (arm64) pnpm build:unpack # Package without installer (for debugging) + +# Command line wake-up +pnpm ztools # Wake up a running instance (or start the app) ``` ### Debugging diff --git a/build/deb-after-install.sh b/build/deb-after-install.sh new file mode 100755 index 00000000..4728e606 --- /dev/null +++ b/build/deb-after-install.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# ZTools deb 安装后脚本:在保留默认后置处理的基础上, +# 将 /usr/bin/ztools 符号链接指向命令行唤起启动器。 + +if type update-alternatives 2>/dev/null >&1; then + # Remove previous link if it doesn't use update-alternatives + if [ -L '/usr/bin/${executable}' -a -e '/usr/bin/${executable}' -a "`readlink '/usr/bin/${executable}'`" != '/etc/alternatives/${executable}' ]; then + rm -f '/usr/bin/${executable}' + fi + update-alternatives --install '/usr/bin/${executable}' '${executable}' '/opt/${sanitizedProductName}/${executable}' 100 || ln -sf '/opt/${sanitizedProductName}/${executable}' '/usr/bin/${executable}' +else + ln -sf '/opt/${sanitizedProductName}/${executable}' '/usr/bin/${executable}' +fi + +# Check if user namespaces are supported by the kernel and working with a quick test: +if ! { [[ -L /proc/self/ns/user ]] && unshare --user true; }; then + # Use SUID chrome-sandbox only on systems without user namespaces: + chmod 4755 '/opt/${sanitizedProductName}/chrome-sandbox' || true +else + chmod 0755 '/opt/${sanitizedProductName}/chrome-sandbox' || true +fi + +if hash update-mime-database 2>/dev/null; then + update-mime-database /usr/share/mime || true +fi + +if hash update-desktop-database 2>/dev/null; then + update-desktop-database /usr/share/applications || true +fi + +# 刷新 hicolor 图标缓存,缺失命令时静默跳过。 +if hash gtk-update-icon-cache 2>/dev/null; then + gtk-update-icon-cache -f /usr/share/icons/hicolor || true +fi + +# 安装 apparmor profile(Ubuntu 24+),与默认模板保持一致。 +if apparmor_status --enabled > /dev/null 2>&1; then + APPARMOR_PROFILE_SOURCE='/opt/${sanitizedProductName}/resources/apparmor-profile' + APPARMOR_PROFILE_TARGET='/etc/apparmor.d/${executable}' + if apparmor_parser --skip-kernel-load --debug "$APPARMOR_PROFILE_SOURCE" > /dev/null 2>&1; then + cp -f "$APPARMOR_PROFILE_SOURCE" "$APPARMOR_PROFILE_TARGET" + + if ! { [ -x '/usr/bin/ischroot' ] && /usr/bin/ischroot; } && hash apparmor_parser 2>/dev/null; then + apparmor_parser --replace --write-cache --skip-read-cache "$APPARMOR_PROFILE_TARGET" + fi + else + echo "Skipping the installation of the AppArmor profile as this version of AppArmor does not seem to support the bundled profile" + fi +fi + +# 命令行唤起启动器:/usr/bin/ztools -> /opt//resources/ztools/ztools-launcher.sh +ztools_launcher='/opt/${sanitizedProductName}/resources/ztools/ztools-launcher.sh' +if [ -f "$ztools_launcher" ]; then + chmod +x "$ztools_launcher" + ln -sf "$ztools_launcher" '/usr/bin/ztools' +fi diff --git a/build/deb-after-remove.sh b/build/deb-after-remove.sh new file mode 100755 index 00000000..115ec2d7 --- /dev/null +++ b/build/deb-after-remove.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# ZTools deb 卸载后脚本:移除命令行唤起命令,并保留默认清理逻辑。 + +# 只移除指向 ZTools 启动器的 /usr/bin/ztools,避免误删用户自建的同名命令。 +ztools_launcher='/opt/${sanitizedProductName}/resources/ztools/ztools-launcher.sh' +if [ -L '/usr/bin/ztools' ] && [ "$(readlink '/usr/bin/ztools')" = "$ztools_launcher" ]; then + rm -f '/usr/bin/ztools' +fi + +# Delete the link to the binary +if type update-alternatives >/dev/null 2>&1; then + update-alternatives --remove '${executable}' '/usr/bin/${executable}' +else + rm -f '/usr/bin/${executable}' +fi + +APPARMOR_PROFILE_DEST='/etc/apparmor.d/${executable}' + +# Remove apparmor profile. +if [ -f "$APPARMOR_PROFILE_DEST" ]; then + rm -f "$APPARMOR_PROFILE_DEST" +fi + +# 刷新 hicolor 图标缓存,缺失命令时静默跳过。 +if hash gtk-update-icon-cache 2>/dev/null; then + gtk-update-icon-cache -f /usr/share/icons/hicolor || true +fi diff --git a/build/icons/128x128.png b/build/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..1db1c63bbbeb1571b273bff36fd054bdbba754eb GIT binary patch literal 11179 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 bGJiNJ(Jym|hRhb=l%p&US|7Mi@`(L^rW=%h literal 0 HcmV?d00001 diff --git a/build/icons/32x32.png b/build/icons/32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..b24f401d90d9527368af405f530037fb4b43d551 GIT binary patch literal 2423 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 literal 0 HcmV?d00001 diff --git a/build/icons/48x48.png b/build/icons/48x48.png new file mode 100644 index 0000000000000000000000000000000000000000..2ee1fcec5c2e7b043d745aaa661815132b6097dc GIT binary patch literal 3729 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 literal 0 HcmV?d00001 diff --git a/build/icons/512x512.png b/build/icons/512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..d90896958f6f20d5fdd4014ae5334dd3c98bd11f GIT binary patch literal 52523 zcmXV11z42N*WFzjK{}+BZj|m0=@5`EB}BSGSU?0pQc7u2N+d+O7C}H76zT3zx>qX@L%GaC`Wga${~^A0ciZ!h1;!O$xmj$Y&7v3btEy3n6y){$CB% zu_F|)!xcvDHLQ7yzL=vz@qc2QL){`KhT$|)RWCNv@zx+s3K{$9(5Uc&V?v2eg>C`} z@nZ#SeCaB}0tT+rwzzP6Th8jPAU7XtiwOP>j!ncsnjuwPtL}M@3Ijg&JqUKBqt;3E z@#3H0;oD7{u-x#raM|2Rrr~m|(S^GxzL=j0lnAPNRt8PRyVcOgBwu{25mHq);i3u# ze)qo9^mWj@C$;U_9Y4uANa+Tj03jryBQ>d#xy zax4Ye+Ca|5I0st)QE04BvNYz8m0fo?BmYpadXG(X{*C>TlP$qP>aow)76=03L%6b{cE}son+d!?(w^6K)Y*5v-x|=d8^F`S{evU8@fk{q-0w ze4r}^gnW4II2%p!>4w?lGFsx(qtuElq)-;pTJ2xrubVGSU`bxgOSA}nvTtmem0nXAr_1b0#nQiz6^nc=9s zpUTcC38r|R_kqL9T2XS0A=0eFsl!RL%#WP$zk`w`xSjpu>5glKa5z&(Uk8Co*EP;V ze1vuDbzmXhGq#`#(e?FpN-C( zXUSnoU4Pm3a+i`(eJTxW-IJ9KJ1-Az7G%O*vK}td-;Uwt;jtge68&P;l2PY6S>rx0 zHBn_V5=~4$Rc(hg+u%`BSQzOk@%N_IZI*TU!c@kIz zXPy!#OwpEumR?>`X^C}-0%@D0pYrcIdL>CT_$Q`HeQH4g+IRn?uzJd^L^G^f+p_&zDx=g~l{ z{+6xd!pD!5B+P3`jW5mc$&)9^AQT)OIsVNyIPJHcJ65%w+2@Cz*TKvHe ziPUl(OxnG+8JWT!#FoP@<|#6n6rslXFJy1twkysBf0+ojS>juIz7$wk


B5b*F1 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 b)=s8=SiA9Kc8AP}g8%e&jI~S8+eiLCq*A%p literal 0 HcmV?d00001 diff --git a/build/icons/64x64.png b/build/icons/64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..8b3313227e134ddd101fa4b556402dc56704ab96 GIT binary patch literal 5190 zcmV-M6uIk(P)5RlcaEwtSqVP3Tam{s|Kt>1%V=lhlWSE)b?JiwtC@GEiA6J z^e*e8ZEPN%``Z)k20ptSk0&pb%9R<(@pbbEC zRaI5Xf&~kVKI=*Vh%sgiC%tT#ft!pm0)WQGMrGW%abE&(JAh&U|J>c(Jt`CmWksV= z#9}dMng#%nWf^w69S(;BKA-QL+wDFB;0S_^ z5fuYi1mN!0*47)Fnwrqi(16CqMw~o(5*-~Kh(sc!>pGdHNdUk(XObi_RaIGbb~eV0 z8H4fT$D^pI2xG^NMP6RsF#x*(yuui(A8haI$^atb=#_O703)LN06ZKF1}E3o*JJ0- zooHxippK3X0{~MLg(-@{BuQeNb1=sGt_RaJq3b#&5(!Eq5(I#_WJ^;@%#Ng)U8{$ZpDrrJE*z2*|6DcTvb&rNfH=i zFijIglmR${kTixc#=toT5kc2=L?RJ09*>(NM~)Qp=g((LmMp>O(WBo1@FT|9uB(v- zcA3Ca_b(EOjBz*|KRtW)Z0Uv#8?bfjR-?PS8y=5GD2kFg(cu`D#3T|4oIiiwaJ$`D zvSf*P@WBT$V#J85Xf*mgRaH;+)kDL|fFyV@#)yc@0IaL7t{(B^lTVs$ZEf)Rd|U|8 z@1%!SV<#oUFbsr3Au~5O7f(F#1fM&1?pXjU7-KJ|(>ZJmAR?Z2*Z(^l4ljT7(MPd! z=T6=0^-4CI4TfP{GvEU;LI{C)JdUoeF1@6rM0)PI=iqX=e!&>~4@)-;MKAE7g0HEm zaS_q3W5g)Ep_tOW16N3P17KRKu%7Mv}eyAnl^2kaqQT! zuNg?rw9`|aYTOP6Y#bJ^*1LesQ<+WdMCB7!W- z=;`T!X`0%WEnDQJ|fF9Sw_?=S-l5LV2nXGOgLl-XFEFZ&;#E>Q`5(Axm@T9 zg^*uRfQ_3rA(lvB;l1~Qh%%TUNfJ6bI?Q|Sxd$6JZafcQJb>n8q?$TOslUWAlZbw~ zYu7IC_U+q^kt0V6OW~!XF-;Q=)d9EL4I2Ss7}Ws-paW00{J(LM$@OCrmhJCOG;9&V$9CY=G(V#H|ETlZ3(;U5DTA7pqsVHe#{ZJR+Kr#3%+bgH%~rDF8fEUS3YE zt*uZLWk3ffrEmlX=NyV6LlJyfrX)C=vV_l0pF!E;ML5*hh(I8KU@(ZA$Bo0w+qU7K zMt%-G2@RZcAdQ}Z9419k(AwIH^73*5@JtdTW$1u3tv@dq4DKi@Dl%kQ7QJ_}zit*n z;QaaXShs#XrcaxOn65(zsdwGp9}t<~jQ8$jDZ|tqPo5X`Dn8Cw>T{d{EzQk%!-5VK6>01l220ydis z!C(-RZo3V?si=TAD+@8hgv2>?!+>292zGSf{-sNC@S~4lx7#mDi(wd0Rn@Gkt7BPN zSzl(1H4u@I((KmK+S*!n`t)fdFE5V|r2P60Ns@Z+;ZVG|L6|U5_?$d3OrA0Yn>TNU z%k9np??{4QvbYQ%9C#mIuNQ`K@mz?aC^&ukv{_qQE6tfRXDNUN08C1rz5<}6s;UY$ zn~e`8re};H5{Y0aj2dj1rfK=`DJM&oWdwskOuK73Ui#mc;80b>`+#q44`RuZGJN#G z2k`oQ(EFO{LH4rQY`m(f3UlVnDIucoF~&4Y2Yk7`z5Shu6DN|c>+DjeYJp$*-A8fD zEfWw=hNk^aJk9yZaFQ6HYdW&rF1**!fS*5C4k08+l7zFtAig$xHh#V7*HG*>Xod-q z2EMtq4T~2o#Noz9czwRhib#`gGu74Av7DToTNz{TT8_}nhQ}4WD2{`ic$Jn-YD@>DMx65d2Yr~v*^RV&77a?S&ANbGCoI%;5ML2ro z2(rB1jEHn7!%~Ka43#R&lVitH;ICBY*lad-;J^U@xVfgLMv|sX znUdSx-97U7@#BzXnGH3mNu~+A-44zUMc z%b}SjOt1{_AAj-*7T#BiQ>Q+I$K!#PNMJArZ72+oWtkm6ejMH1-6N+=nUc$h=*uTg zoT#5Xc``E$BbAT8loHf+9XF2~hroytFj7*(l2I^a7(2##LqA5qJMYvZ7K=euRkXFW z;X98!f}cG5EE0wZll6LqdeX@`a`a;?D7_CY&CPJT-B)FC(sK(!2<+RpkMi^LizNWL zkw}Dv!(nndokP9?LI`|#-~hC~-eP*-pZ+Xq8C)(G>~=d^TU)W>yDRbZlRrY-Fo8bc z4;}d{7Tk9q+FD!Sa=Wis?EBKqIY&4grbr~h0OU#lygfZVNF);EbUKG#CooOe9S%4y zBQ0hzs=d7(kF8pT|602ial-%@Pf3+3OZf0$BNi2#9Q=^U~d9BM_^G)?sM^x&7zKaX#G{p*NoIz%#s zZGnIDPc>MwcrkQcgQ}{CMxzLa!$Kx6Dff$AXr_;%tPA36qlK`CY`~BS`MvU;B zK7E>OHrrKS1&1Sort`P_!pzAu~i3B#j^b!`8mLj6*sp5qNzUp^7@U3P4*6T^CDngx|xM9o~ zyjoR-n{T>l$a|h?Ysbb78;0DA5R1jAprC+xJf3C%Z9G{mIXq#)1jOTUx|(_)=NySd z0-C1bHx(6wg5UD;%lPK~UxyF^4u=Dsot?NLKOfbvzmA)4x(VTghN!M1q76R7@dR{T zM^{%@zhv#jf}wakP7@|f0D!}cG2My=>nBZ`1WJDDYQUSOiHhHD#k@Il1_Zz9rI+yF zvSq16NPAlw#^mQ?NA+vCv7i9mu{Z?h80sy&f@qK?O_~JscA%vSBl`*q3lRteL?V$G za+)h$?Bns?Fn8;=ZJ0H4W(qt3Fq7c_XTytFx#Hj9c6%U%KsX%6%DF6A-v1ZjOc)i|~L@>sXmzRg1{`gsR_e3DXg_CFL6;n3Q z-hjjzI=i}3X7FMRNX7u>tXZ>oZf@=g03QHgQZg%?AflaRWo65&tE)}M7$1ryu)uT9 zvF+7L%$$B#26!?}I3x*AKl@`m@%Vqj@Aqe%lq}1reDzgauG9@?1}|j+5{ZOaR#wIV z>`Y>oGScFqQ0U!pPY+KoMGv-dJRXOlD0ro+3U}RkXBv1w@LutH^697XJrD?x&*vL( z`u%>&@^~mOFOUATZyym6^&~Wkn?y0gq?m3{cTW#}`{9QP013{?GT*~JIp^f{dIzP| z=krk@5Fh~hnl)=oB6>R=bHCD9YisL{v17*?k|c?XiGC;)!luoe@y(@6(UZ_1%kqV+ zi{NNHbQo2yzJi>bT+C2!XDy5MJ570}JNQLsU2V*99&!NV$^yo?USK_1Gu4W70(Ey5292v0QS8 z!(p5_aYCr7I+HQ>hxASno~9^_3UnL@#)`|4#;7V6g*0sv$FHR;#Ed^+;W3}X@_ZSfXi7|A9LfBqe znGp>NArOnj5C{ac2kTq<#JIvjj}8!jvhTqM6@m)(-k#J?%lh0=CoN`rV#L_{VLp|-ZRbn4Wpf2^*qE}lDguI6w!WJ!`>7)EOTJq#-k z5sgN*>gsBF>eQ+AwY9aS$q1JQYo8xdv-JM``+Z;e%2)Qh@x~j)ci(-tMntJ*>A#0z zHA^$bwB5UR%hRV%uYc>Uw`Sdb`|Y8jw#{FZ2>_66m*0Kx;6WNSYLpJZbi3W8swz1g zj=#xNRn^i(=BQDl^n(Wv5)tiAHc4YB?edq!1oXZzu`Uz}QAtS&0nlVwCYQ@acDwz$ zPM6C?vMdt-t)!%cLZOhQ@Yt||Pgjg-{R3r06x_UdGx_~~1At+(*~sa1lHG16o6UAj zrrmBQr_)I`n~eY%e!t(?ym>PbQ84)$PX?XWLgO+kgp5QYV~D7-rKN>dtXM&^Eb9Pt zAw=q03>R=q+=UPXK$m4%U$J5ZwY0Pl5miPakujDJbIri_b(Ye8pG`#dO-)U-a^*_$ z`Fth-4S*p@Qi^y>G89F*=(JASVxlBT1i%2G`FuWe<;s=R)YL>oRG)lhuOCgs*uQ^28X6kV)YOFb_I4}%-nZ(( zY&ILq$;rXkv13tGRD|1aza3xv;ukS`^yt68S7!&g78iYa`rNs5qk_R;RyZ6+vL^vR zZxf@_>4eMWLLd-0mz90UguN{E;vxX8-^I07*qoM6N<$f}^a? A0{{R3 literal 0 HcmV?d00001 diff --git a/build/pacman-after-install.sh b/build/pacman-after-install.sh new file mode 100755 index 00000000..aa7ddd38 --- /dev/null +++ b/build/pacman-after-install.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# ZTools pacman 安装后脚本:创建命令行唤起启动器符号链接, +# 并刷新桌面数据库与图标缓存,确保应用菜单显示 ZTools 图标。 + +# 命令行唤起启动器:/usr/bin/ztools -> /opt//resources/ztools/ztools-launcher.sh +ztools_launcher='/opt/${sanitizedProductName}/resources/ztools/ztools-launcher.sh' +if [ -f "$ztools_launcher" ]; then + chmod +x "$ztools_launcher" + ln -sf "$ztools_launcher" '/usr/bin/ztools' +fi + +# 刷新桌面入口数据库,缺失命令时静默跳过。 +if hash update-desktop-database 2>/dev/null; then + update-desktop-database /usr/share/applications || true +fi + +# 刷新 hicolor 图标缓存,缺失命令时静默跳过。 +if hash gtk-update-icon-cache 2>/dev/null; then + gtk-update-icon-cache -f /usr/share/icons/hicolor || true +fi diff --git a/build/pacman-after-remove.sh b/build/pacman-after-remove.sh new file mode 100755 index 00000000..7dd8d283 --- /dev/null +++ b/build/pacman-after-remove.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# ZTools pacman 卸载后脚本:移除命令行唤起启动器符号链接, +# 并刷新桌面数据库与图标缓存。 + +# 只移除指向 ZTools 启动器的 /usr/bin/ztools,避免误删用户自建的同名命令。 +ztools_launcher='/opt/${sanitizedProductName}/resources/ztools/ztools-launcher.sh' +if [ -L '/usr/bin/ztools' ] && [ "$(readlink '/usr/bin/ztools')" = "$ztools_launcher" ]; then + rm -f '/usr/bin/ztools' +fi + +# 刷新桌面入口数据库,缺失命令时静默跳过。 +if hash update-desktop-database 2>/dev/null; then + update-desktop-database /usr/share/applications || true +fi + +# 刷新 hicolor 图标缓存,缺失命令时静默跳过。 +if hash gtk-update-icon-cache 2>/dev/null; then + gtk-update-icon-cache -f /usr/share/icons/hicolor || true +fi diff --git a/build/ztools-launcher.sh b/build/ztools-launcher.sh new file mode 100755 index 00000000..ba74f913 --- /dev/null +++ b/build/ztools-launcher.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env sh + +# ZTools 命令行唤起启动器。 +# 打包时被 electron-builder 放入应用 resources/ztools/ 目录, +# 安装器会将 /usr/bin/ztools 符号链接指向本脚本。 + +# 脚本实际所在目录(resources/ztools)。通过符号链接调用时先解析真实路径。 +SCRIPT_PATH=$0 +if [ -L "$SCRIPT_PATH" ]; then + SCRIPT_PATH=$(readlink -f "$SCRIPT_PATH") +fi +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$SCRIPT_PATH")" && pwd) + +for BIN in \ + "$SCRIPT_DIR/../MacOS/ZTools" \ + "$SCRIPT_DIR/../ztools" \ + "$SCRIPT_DIR/../ZTools" +do + if [ -x "$BIN" ]; then + exec "$BIN" --ztools-wake + fi +done + +echo "未找到 ZTools 可执行文件(已探测 $SCRIPT_DIR/../MacOS/ZTools、$SCRIPT_DIR/../ztools、$SCRIPT_DIR/../ZTools)" >&2 +exit 1 diff --git a/changelog.md b/changelog.md index 97fb79a4..acc86961 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,10 @@ - 更新窗口下载中支持取消 - 添加getUserTempToken插件api - 支持私有部署同步服务 +- 数据同步支持在官方服务与私有部署服务之间切换,可配置私有服务器地址、测试连接,并使用独立账号登录和注销 +- 切换私有同步服务器时自动重置对应服务的本机同步进度,确保本地文档和附件完整进入新服务的同步队列 +- 新增「使用 CSS -webkit-app-region 拖拽」选项,Wayland 环境下可由系统接管搜索框区域拖拽窗口 +- 新增 `ztools` 命令行唤起:已运行时直接呼出主窗口,未运行时自动启动并显示窗口(Linux deb 自动安装命令,macOS 手动软链接) ## 修复 (Fix) diff --git a/docs/examples/provider-example/README.md b/docs/examples/provider-example/README.md index bfa4e6b6..e55027b7 100644 --- a/docs/examples/provider-example/README.md +++ b/docs/examples/provider-example/README.md @@ -21,8 +21,12 @@ ``` ```js -ztools.registerProvider('baidu', async (input) => { /* ... */ }) -ztools.registerProvider('google', async (input) => { /* ... */ }) +ztools.registerProvider('baidu', async (input) => { + /* ... */ +}) +ztools.registerProvider('google', async (input) => { + /* ... */ +}) ``` 详见主程序 `docs/provider-development-guide.md`。 diff --git a/docs/provider-development-guide.md b/docs/provider-development-guide.md index d1508d36..65edfa4c 100644 --- a/docs/provider-development-guide.md +++ b/docs/provider-development-guide.md @@ -50,7 +50,7 @@ ZTools 把「翻译」「OCR」等能力抽象为 **Provider(提供商)**。 ```json { "providers": { - "baidu": { "type": "translation", "label": "百度翻译" }, + "baidu": { "type": "translation", "label": "百度翻译" }, "google": { "type": "translation", "label": "谷歌翻译" } } } @@ -58,8 +58,12 @@ ZTools 把「翻译」「OCR」等能力抽象为 **Provider(提供商)**。 ```js // preload.js -ztools.registerProvider('baidu', async (input) => { /* 调用百度 */ }) -ztools.registerProvider('google', async (input) => { /* 调用谷歌 */ }) +ztools.registerProvider('baidu', async (input) => { + /* 调用百度 */ +}) +ztools.registerProvider('google', async (input) => { + /* 调用谷歌 */ +}) ``` 两条都会出现在「设置 → 提供商 → 翻译」,用户可分别启用并选其中一个为默认。 diff --git a/electron-builder.yml b/electron-builder.yml index 4f0e46fd..4a3a74be 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -80,13 +80,31 @@ mac: dmg: artifactName: ${name}-${version}-${os}-${arch}.${ext} linux: + # 显式指向多尺寸图标集 build/icons/(16x16…512x512), + # 避免自动发现优先命中 build/icon.png 而只向 hicolor 安装单尺寸图标。 + icon: build/icons target: - AppImage - deb + - pacman maintainer: electronjs.org category: Utility appImage: artifactName: ${name}-${version}-${os}-${arch}.${ext} npmRebuild: false +extraResources: + - from: build/ztools-launcher.sh + to: ztools/ztools-launcher.sh + filter: + - '**/*' + +deb: + afterInstall: build/deb-after-install.sh + afterRemove: build/deb-after-remove.sh + +pacman: + afterInstall: build/pacman-after-install.sh + afterRemove: build/pacman-after-remove.sh + afterPack: ./build/afterPack.js diff --git a/internal-plugins/setting/src/env.d.ts b/internal-plugins/setting/src/env.d.ts index da5230c6..971135cc 100644 --- a/internal-plugins/setting/src/env.d.ts +++ b/internal-plugins/setting/src/env.d.ts @@ -394,6 +394,7 @@ declare global { updateAutoClear: (autoClear: string) => Promise updateAutoBackToSearch: (autoBackToSearch: string) => Promise updateWindowPositionStrategy: (strategy: string) => Promise + updateCssAppRegionDrag: (enabled: boolean) => Promise updateShowRecentInSearch: (showRecentInSearch: boolean) => Promise updateMatchRecommendation: (showMatchRecommendation: boolean) => Promise updateLocalAppSearch: (enabled: boolean) => Promise @@ -425,6 +426,7 @@ declare global { getAppName: () => Promise getSystemVersions: () => Promise getPlatform: () => NodeJS.Platform + getLinuxSession: () => { isWayland: boolean } isWindows11: () => Promise // 软件更新 diff --git a/internal-plugins/setting/src/views/GeneralSetting/GeneralSetting.vue b/internal-plugins/setting/src/views/GeneralSetting/GeneralSetting.vue index 88b78f70..45e83fd8 100644 --- a/internal-plugins/setting/src/views/GeneralSetting/GeneralSetting.vue +++ b/internal-plugins/setting/src/views/GeneralSetting/GeneralSetting.vue @@ -115,6 +115,8 @@ const superPanelMouseButtonOptions = [ // 当前平台(与 window.ztools.getPlatform 返回类型保持一致) const platform = ref<'darwin' | 'win32' | 'linux'>('darwin') +// 当前会话是否为原生 Wayland(仅 Linux 下用于悬浮球置顶限制说明) +const isWayland = ref(false) // 终端打开设置 const terminal = ref('default') @@ -179,6 +181,8 @@ const autoPaste = ref('3s') const autoClear = ref('immediately') const autoBackToSearch = ref('never') const windowPositionStrategy = ref('remember') +// Wayland 下由系统接管搜索框区域拖拽(仅 Linux 平台显示) +const useCssAppRegionDrag = ref(false) const showRecentInSearch = ref(true) const showMatchRecommendation = ref(true) const localAppSearch = ref(true) @@ -575,6 +579,21 @@ async function handleWindowPositionStrategyChange(): Promise { } } +/** + * 处理 CSS app-region 拖拽开关变化:先持久化,再通知主渲染进程切换拖拽方式。 + * @returns 保存和通知完成后结束的 Promise + */ +async function handleUseCssAppRegionDragChange(): Promise { + try { + await saveSettings() + // 通知主渲染进程实时切换拖拽方式 + await window.ztools.internal.updateCssAppRegionDrag(useCssAppRegionDrag.value) + console.log('CSS app-region 拖拽配置已更新:', useCssAppRegionDrag.value) + } catch (error) { + console.error('保存 CSS app-region 拖拽配置失败:', error) + } +} + // 处理主题变化 async function handleThemeChange(): Promise { try { @@ -1266,13 +1285,18 @@ function isValidProxyUrl(url: string): boolean { // ==================== 自定义颜色相关辅助函数 ==================== -// 获取平台信息(用于快捷键文案) +/** + * 获取平台信息并探测 Wayland 会话(用于快捷键文案与悬浮球置顶限制说明)。 + * @returns 平台与会话信息获取完成后结束的 Promise + */ async function getPlatformInfo(): Promise { try { const pf = await window.ztools.internal.getPlatform() if (pf === 'darwin' || pf === 'win32' || pf === 'linux') { platform.value = pf } + const session = window.ztools.internal.getLinuxSession() + isWayland.value = session?.isWayland === true } catch (error) { console.error('获取平台信息失败:', error) } @@ -1301,6 +1325,7 @@ async function loadSettings(): Promise { autoClear.value = data.autoClear ?? 'immediately' autoBackToSearch.value = data.autoBackToSearch ?? 'never' windowPositionStrategy.value = data.windowPositionStrategy ?? 'remember' + useCssAppRegionDrag.value = data.useCssAppRegionDrag ?? false showRecentInSearch.value = data.showRecentInSearch ?? true showMatchRecommendation.value = data.showMatchRecommendation ?? true localAppSearch.value = data.localAppSearch ?? true @@ -1397,6 +1422,7 @@ async function saveSettings(): Promise { autoClear: autoClear.value, autoBackToSearch: autoBackToSearch.value, windowPositionStrategy: windowPositionStrategy.value, + useCssAppRegionDrag: useCssAppRegionDrag.value, showRecentInSearch: showRecentInSearch.value, showMatchRecommendation: showMatchRecommendation.value, localAppSearch: localAppSearch.value, @@ -2119,6 +2145,25 @@ onUnmounted(() => { +
+
+ 使用 CSS -webkit-app-region 拖拽 + Wayland 下自定义拖拽可能失效,开启后由系统接管搜索框区域拖拽窗口 +
+
+ +
+
+
插件默认高度 @@ -2320,6 +2365,19 @@ onUnmounted(() => {
+
+
+ Wayland 置顶说明 + 原生 Wayland 协议没有置顶接口,悬浮球无法保持在其它窗口之上(X11 会话不受影响)。KDE + Plasma:系统设置 → 窗口管理 → 窗口规则 → 新建规则,检测窗口属性选中「ZTools + 悬浮球」,将「在其他窗口上方」设为「强制」。GNOME:系统未内置置顶功能,可安装第三方「始终置顶」类扩展,或改用 + X11/XWayland 会话运行 ZTools。Hyprland / Sway 等 wlroots + 系合成器:多数不支持置顶协议,建议使用 X11/XWayland 会话。 +
+
+
悬浮球文字 diff --git a/package.json b/package.json index 7ab6877e..bd4f9db4 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "build:linux:x64": "cross-env ZTOOLS_TARGET_PLATFORM=linux pnpm build && electron-builder --linux --x64", "build:linux:arm64": "cross-env ZTOOLS_TARGET_PLATFORM=linux pnpm build && electron-builder --linux --arm64", "updater": "node scripts/updater.mjs", + "ztools": "node scripts/ztools-cli.mjs", "test": "vitest run", "test:e2e": "pnpm exec electron-vite build && playwright test --config=playwright.e2e.config.ts", "test:windows-scanner": "electron scripts/compare-windows-scanner.cjs", @@ -47,6 +48,9 @@ "hooks:install": "git config core.hooksPath .githooks", "postinstall": "pnpm run hooks:install || true" }, + "bin": { + "ztools": "scripts/ztools-cli.mjs" + }, "dependencies": { "@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/utils": "^4.0.0", diff --git a/resources/floatingBall.html b/resources/floatingBall.html index 2dc5a5dd..489746bb 100644 --- a/resources/floatingBall.html +++ b/resources/floatingBall.html @@ -127,6 +127,17 @@ animation: border-spin 2s linear infinite; } + /* 移动模式:整球作为合成器拖拽区域(Wayland CSS app-region) */ + .floating-ball.move-mode { + -webkit-app-region: drag; + cursor: move; + } + + /* 移动模式内圈高亮,提示当前处于系统拖拽状态 */ + .floating-ball.move-mode .floating-ball-inner { + background: rgba(80, 130, 255, 0.4); + } + /* 文件拖入悬停效果 */ .floating-ball.file-hover::before { animation: border-spin 1s linear infinite; @@ -153,6 +164,7 @@ let isDragging = false let startScreenX = 0 let startScreenY = 0 + let moveMode = false // 移动模式:整球交给合成器拖拽,跳过手动拖拽与点击逻辑 const DRAG_THRESHOLD = 5 // 双击检测 @@ -160,7 +172,7 @@ const DOUBLE_CLICK_DELAY = 300 // 双击间隔 300ms ball.addEventListener('mousedown', (e) => { - if (e.button !== 0) return + if (e.button !== 0 || moveMode) return isMouseDown = true isDragging = false startScreenX = e.screenX @@ -170,7 +182,7 @@ }) document.addEventListener('mousemove', (e) => { - if (!isMouseDown) return + if (!isMouseDown || moveMode) return const dx = e.screenX - startScreenX const dy = e.screenY - startScreenY if (!isDragging && Math.sqrt(dx * dx + dy * dy) >= DRAG_THRESHOLD) { @@ -198,6 +210,9 @@ if (isDragging) { isDragging = false ipcRenderer.send('floating-ball-drag-end') + } else if (moveMode) { + // 移动模式下不派发单击/双击,等待主进程退出移动模式 + return } else { // 检测双击 const now = Date.now() @@ -219,6 +234,17 @@ ipcRenderer.send('floating-ball-contextmenu') }) + // ==================== 移动模式(Wayland 合成器拖拽) ==================== + ipcRenderer.on('floating-ball-enter-move-mode', () => { + moveMode = true + ball.classList.add('move-mode') + }) + + ipcRenderer.on('floating-ball-exit-move-mode', () => { + moveMode = false + ball.classList.remove('move-mode') + }) + // ==================== 接收文字配置 ==================== const letterEl = document.querySelector('.floating-ball-letter') ipcRenderer.on('floating-ball-set-letter', (_event, letter) => { diff --git a/resources/preload.js b/resources/preload.js index 8a01a0d0..97b6a053 100644 --- a/resources/preload.js +++ b/resources/preload.js @@ -1073,6 +1073,11 @@ window.ztools = { getAppVersion: async () => await electron.ipcRenderer.invoke('get-app-version'), getSystemVersions: async () => await electron.ipcRenderer.invoke('get-system-versions'), getPlatform: () => electron.ipcRenderer.sendSync('internal:get-platform'), + /** + * 返回当前 Linux 会话是否为 Wayland。 + * @returns {{ isWayland: boolean }} Wayland 会话判定结果 + */ + getLinuxSession: () => electron.ipcRenderer.sendSync('internal:get-linux-session'), // 通知主渲染进程更新搜索框提示文字 updatePlaceholder: async (placeholder) => @@ -1091,6 +1096,9 @@ window.ztools = { await electron.ipcRenderer.invoke('internal:update-auto-back-to-search', autoBackToSearch), updateWindowPositionStrategy: async (strategy) => await electron.ipcRenderer.invoke('internal:update-window-position-strategy', strategy), + // 通知主渲染进程切换 CSS app-region 拖拽 + updateCssAppRegionDrag: async (enabled) => + await electron.ipcRenderer.invoke('internal:update-css-app-region-drag', enabled), // 通知主渲染进程更新显示最近使用配置 updateShowRecentInSearch: async (showRecentInSearch) => await electron.ipcRenderer.invoke( diff --git a/scripts/ztools-cli.mjs b/scripts/ztools-cli.mjs new file mode 100755 index 00000000..d4a3d49d --- /dev/null +++ b/scripts/ztools-cli.mjs @@ -0,0 +1,84 @@ +#!/usr/bin/env node + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ + +import { spawn } from 'node:child_process' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const WAKE_ARG = '--ztools-wake' +const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') + +/** + * 定位 ZTools 可执行文件。 + * + * 优先使用 ZTOOLS_BIN 环境变量;其次探测常见打包安装位置; + * 最后回退到开发环境中的 electron 二进制。 + * + * @returns 可执行文件绝对路径;找不到时返回 null。 + */ +function resolveZToolsBinary() { + if (process.env.ZTOOLS_BIN) return process.env.ZTOOLS_BIN + + const candidates = [] + if (process.platform === 'darwin') { + candidates.push('/Applications/ZTools.app/Contents/MacOS/ZTools') + } else if (process.platform === 'linux') { + candidates.push('/opt/ZTools/ztools') + candidates.push('/opt/ZTools/ZTools') + candidates.push('/usr/lib/ztools/ztools') + } else if (process.platform === 'win32') { + candidates.push(path.join(projectRoot, 'dist', 'win-unpacked', 'ZTools.exe')) + } + + for (const candidate of candidates) { + if (fs.existsSync(candidate)) return candidate + } + + // 开发环境:使用项目本地的 electron 二进制(需要先构建 out/main)。 + try { + return require.resolve('electron') + } catch { + return null + } +} + +function printHelp() { + console.log(`用法: ztools [选项] + +唤起已运行的 ZTools 实例并显示主窗口;如果应用尚未运行则先启动。 + +选项: + --help 显示本帮助信息 + +环境变量: + ZTOOLS_BIN 指定 ZTools 可执行文件路径(打包后的应用或 electron 二进制)`) +} + +const args = process.argv.slice(2) +if (args.includes('--help') || args.includes('-h')) { + printHelp() + process.exit(0) +} + +const binary = resolveZToolsBinary() +if (!binary) { + console.error( + '未找到 ZTools 可执行文件。请先执行 pnpm build 构建应用,或通过 ZTOOLS_BIN 指定二进制路径。' + ) + process.exit(1) +} + +// 开发环境使用本地 electron,需要把项目目录作为应用入口传入; +// 打包后的二进制则直接携带唤醒参数启动。 +const launchArgs = binary.includes('electron') ? [projectRoot, WAKE_ARG] : [WAKE_ARG] + +console.log(`[ztools] 启动唤醒进程: ${binary}`) +const child = spawn(binary, launchArgs, { + detached: true, + stdio: 'ignore', + windowsHide: true +}) +child.unref() +process.exit(0) diff --git a/src/main/api/plugin/internal.ts b/src/main/api/plugin/internal.ts index e494c816..af698121 100644 --- a/src/main/api/plugin/internal.ts +++ b/src/main/api/plugin/internal.ts @@ -1058,6 +1058,18 @@ export class InternalPluginAPI { return { success: true } }) + // 通知主渲染进程切换 CSS app-region 拖拽(Wayland 下由系统接管窗口移动) + ipcMain.handle('internal:update-css-app-region-drag', async (event, enabled: boolean) => { + if (!requireInternalPlugin(this.pluginManager, event)) { + throw new PermissionDeniedError('internal:update-css-app-region-drag') + } + // 广播到主渲染进程 + this.mainWindow?.webContents.send('update-css-app-region-drag', enabled) + // 同步主进程状态,供 Linux blur 自动隐藏决策使用 + windowAPI.setCssAppRegionDrag(enabled) + return { success: true } + }) + // 通知主渲染进程更新显示最近使用配置 ipcMain.handle( 'internal:update-show-recent-in-search', @@ -1220,6 +1232,19 @@ export class InternalPluginAPI { event.returnValue = process.platform }) + /** + * 返回当前会话是否为原生 Wayland(供设置页判断悬浮球置顶限制)。 + * @param event IPC 事件对象 + * @returns 无返回值,结果通过 event.returnValue 同步返回 + */ + ipcMain.on('internal:get-linux-session', (event) => { + if (!requireInternalPlugin(this.pluginManager, event)) { + event.returnValue = null + return + } + event.returnValue = { isWayland: !!process.env.WAYLAND_DISPLAY } + }) + // ==================== 应用更新 API ==================== ipcMain.handle('internal:updater-check-update', async (event) => { if (!requireInternalPlugin(this.pluginManager, event)) { diff --git a/src/main/api/renderer/settings.ts b/src/main/api/renderer/settings.ts index 74b80508..2740a686 100644 --- a/src/main/api/renderer/settings.ts +++ b/src/main/api/renderer/settings.ts @@ -11,6 +11,7 @@ import { getCurrentShortcut, updateShortcut } from '../../appMain.js' import dndManager from '../../core/dndManager.js' import doubleTapManager from '../../core/doubleTapManager.js' +import { getLinuxLaunchAtLogin, setLinuxLaunchAtLogin } from '../../core/linuxAutoStart.js' import proxyManager from '../../managers/proxyManager.js' import windowManager from '../../managers/windowManager.js' import { primeScreenCaptureFrame } from '../../core/screenCapture' @@ -383,6 +384,17 @@ export class SettingsAPI { // 设置开机启动 public setLaunchAtLogin(enable: boolean): void { + // Linux 打包版通过 XDG autostart 条目控制自启,开发模式不写入用户配置。 + if (process.platform === 'linux') { + if (app.isPackaged) { + setLinuxLaunchAtLogin(enable) + console.log('[Settings] 设置开机启动:', enable) + } else { + console.log('[Settings] Linux 开发模式跳过开机自启设置:', enable) + } + return + } + app.setLoginItemSettings({ openAtLogin: enable, openAsHidden: true @@ -392,6 +404,15 @@ export class SettingsAPI { // 获取开机启动状态 public getLaunchAtLogin(): boolean { + // Linux 打包版读取 XDG autostart 条目,开发模式不读取用户配置。 + if (process.platform === 'linux') { + if (app.isPackaged) { + return getLinuxLaunchAtLogin() + } + console.log('[Settings] Linux 开发模式跳过开机自启读取') + return false + } + const settings = app.getLoginItemSettings() return settings.openAtLogin } diff --git a/src/main/api/renderer/window.ts b/src/main/api/renderer/window.ts index 35c17427..cc9402af 100644 --- a/src/main/api/renderer/window.ts +++ b/src/main/api/renderer/window.ts @@ -176,6 +176,16 @@ export class WindowAPI { await windowManager.updateWindowPositionStrategy(strategy) console.log('[WindowAPI] 更新窗口呼出位置策略:', strategy) } + + /** + * 更新 Linux CSS app-region 拖拽开关状态。 + * + * @param enabled 是否启用 CSS app-region 系统拖拽。 + * @returns 无返回值。 + */ + public setCssAppRegionDrag(enabled: boolean): void { + windowManager.setCssAppRegionDragEnabled(enabled) + } } export default new WindowAPI() diff --git a/src/main/appMain.ts b/src/main/appMain.ts index 83a7343d..ba282ef1 100644 --- a/src/main/appMain.ts +++ b/src/main/appMain.ts @@ -23,6 +23,7 @@ import httpServer from './core/httpServer' import mcpServer from './core/mcpServer' import { registerIconProtocolForSession, registerIconScheme } from './core/iconProtocol' import { getLogsPath } from './core/appData/appDataPaths' +import { isZToolsCliWake } from './core/cliWake' import { loadInternalPlugins } from './core/internalPluginLoader' import pluginManager from './managers/pluginManager' import windowManager from './managers/windowManager' @@ -39,6 +40,10 @@ if (platform.isMacOS) { } app.on('second-instance', (_event, argv) => { + if (isZToolsCliWake(argv)) { + console.log('[Main] 第二实例携带命令行唤起参数 --ztools-wake') + } + // Windows: 检查命令行参数中是否有 .zpx 文件路径 const zpxPath = argv.find((arg) => arg.endsWith('.zpx')) if (zpxPath) { @@ -148,6 +153,9 @@ app.whenReady().then(async () => { if (isE2ETest) { // 测试模式直接展示窗口,让自动化驱动无需依赖全局快捷键。 mainWindow.once('ready-to-show', () => windowManager.showWindow()) + } else if (isZToolsCliWake(process.argv)) { + // 命令行唤起启动:应用未运行时由 CLI 直接启动,就绪后自动显示窗口。 + mainWindow.once('ready-to-show', () => windowManager.showWindow()) } // 初始化 API 和插件管理器 diff --git a/src/main/core/cliWake.ts b/src/main/core/cliWake.ts new file mode 100644 index 00000000..fb4dab5e --- /dev/null +++ b/src/main/core/cliWake.ts @@ -0,0 +1,9 @@ +/** + * 判断进程启动参数是否携带命令行唤起标记。 + * + * @param argv 进程启动参数列表(通常为 process.argv)。 + * @returns 参数列表中存在 --ztools-wake 时返回 true。 + */ +export function isZToolsCliWake(argv: string[]): boolean { + return argv.includes('--ztools-wake') +} diff --git a/src/main/core/floatingBallDragMode.ts b/src/main/core/floatingBallDragMode.ts new file mode 100644 index 00000000..e38e8f41 --- /dev/null +++ b/src/main/core/floatingBallDragMode.ts @@ -0,0 +1,18 @@ +/** + * 判断悬浮球右键菜单是否需要显示“移动悬浮球”入口。 + * + * 仅当 Linux 平台、Wayland 会话且已开启 CSS app-region 系统拖拽时, + * 悬浮球才能交给合成器进行原生拖拽。 + * + * @param isLinux 是否为 Linux 平台。 + * @param isWayland 是否为 Wayland 会话(沿用仓库 WAYLAND_DISPLAY 环境变量约定)。 + * @param cssAppRegionDragEnabled 是否已在设置中开启 CSS app-region 系统拖拽。 + * @returns 是否满足显示“移动悬浮球”菜单项的条件。 + */ +export function shouldShowFloatingBallMoveMenu( + isLinux: boolean, + isWayland: boolean, + cssAppRegionDragEnabled: boolean +): boolean { + return isLinux && isWayland && cssAppRegionDragEnabled +} diff --git a/src/main/core/floatingBallManager.ts b/src/main/core/floatingBallManager.ts index f3bcc9c4..9686bd83 100644 --- a/src/main/core/floatingBallManager.ts +++ b/src/main/core/floatingBallManager.ts @@ -1,7 +1,9 @@ +import { platform } from '@electron-toolkit/utils' import { BrowserWindow, ipcMain, Menu, screen } from 'electron' import floatingBallHtml from '../../../resources/floatingBall.html?asset' import databaseAPI from '../api/shared/database' import windowManager from '../managers/windowManager' +import { shouldShowFloatingBallMoveMenu } from './floatingBallDragMode' // 悬浮球尺寸 const BALL_SIZE = 48 @@ -18,6 +20,10 @@ class FloatingBallManager { // 拖拽状态:记录拖拽开始时鼠标相对窗口左上角的偏移 private dragOffsetX = 0 private dragOffsetY = 0 + // 移动模式:Wayland 下整球交给合成器拖拽的状态标记 + private moveMode = false + // 移动模式退出防抖定时器:move 事件停止 300ms 后自动退出并保存位置 + private moveModeExitTimer: ReturnType | null = null /** * 初始化悬浮球管理器 @@ -163,6 +169,8 @@ class FloatingBallManager { x, y, frame: false, + // 固定窗口标题,便于 KDE 等桌面按「窗口规则」检测悬浮球 + title: 'ZTools 悬浮球', transparent: true, alwaysOnTop: true, resizable: false, @@ -179,6 +187,22 @@ class FloatingBallManager { } }) + // 移动模式监听:moved 触发立即退出,move 停止 300ms 后退出(覆盖不同合成器事件差异) + this.ballWindow.on('moved', () => { + if (this.moveMode) { + this.exitMoveMode() + } + }) + this.ballWindow.on('move', () => { + if (!this.moveMode) return + // 拖拽仍在进行,重置防抖定时器 + this.clearMoveModeExitTimer() + this.moveModeExitTimer = setTimeout(() => { + this.moveModeExitTimer = null + this.exitMoveMode() + }, 300) + }) + // macOS 上设置窗口层级为浮动面板(高于普通窗口) this.ballWindow.setAlwaysOnTop(true, 'floating') // 所有工作空间都可见 @@ -286,9 +310,47 @@ class FloatingBallManager { /** * 显示右键菜单 + * 菜单项按当前平台、Wayland 会话与 CSS 拖拽开关动态构建; + * 移动模式下提供“退出移动模式”入口。 + * + * @returns 无返回值。 */ private showContextMenu(): void { - if (!this.ballWindow) return + if (!this.ballWindow || this.ballWindow.isDestroyed()) return + + // 读取当前会话与 CSS app-region 拖拽开关,决定是否显示移动入口 + const isLinux = platform.isLinux + const isWayland = !!process.env.WAYLAND_DISPLAY + const cssAppRegionDragEnabled = this.isCssAppRegionDragEnabled() + const showMoveEntry = shouldShowFloatingBallMoveMenu( + isLinux, + isWayland, + cssAppRegionDragEnabled + ) + + const moveItems: Electron.MenuItemConstructorOptions[] = [] + if (this.moveMode) { + // 移动模式中始终提供退出入口 + moveItems.push({ + label: '退出移动模式', + click: () => { + this.exitMoveMode() + } + }) + } else if (showMoveEntry) { + moveItems.push({ + label: '移动悬浮球', + click: () => { + this.enterMoveMode() + } + }) + } else if (isLinux && isWayland) { + // Wayland 但未开启 CSS 拖拽时给出禁用提示,引导用户到设置中开启 + moveItems.push({ + label: '需在设置中开启 CSS 拖拽后使用', + enabled: false + }) + } const menu = Menu.buildFromTemplate([ { @@ -297,6 +359,7 @@ class FloatingBallManager { this.handleBallClick() } }, + ...(moveItems.length > 0 ? [{ type: 'separator' as const }, ...moveItems] : []), { type: 'separator' }, { label: '隐藏悬浮球', @@ -309,6 +372,69 @@ class FloatingBallManager { menu.popup({ window: this.ballWindow }) } + /** + * 进入移动模式 + * 发送 IPC 通知悬浮球页面切换为合成器拖拽区域,等待拖拽结束后自动退出。 + * + * @returns 无返回值。 + */ + private enterMoveMode(): void { + if (this.moveMode || !this.ballWindow || this.ballWindow.isDestroyed()) return + + // 清理可能残留的退出定时器,避免旧定时器提前退出 + this.clearMoveModeExitTimer() + this.moveMode = true + this.ballWindow.webContents.send('floating-ball-enter-move-mode') + console.log('[FloatingBall] 已进入移动模式') + } + + /** + * 退出移动模式 + * 清理防抖定时器、通知页面恢复交互并保存当前位置。 + * + * @returns 无返回值。 + */ + private exitMoveMode(): void { + if (!this.moveMode) return + + this.clearMoveModeExitTimer() + this.moveMode = false + if (this.ballWindow && !this.ballWindow.isDestroyed()) { + this.ballWindow.webContents.send('floating-ball-exit-move-mode') + } + // 拖拽结束后保存合成器决定的最终位置 + this.savePosition() + console.log('[FloatingBall] 已退出移动模式') + } + + /** + * 清理移动模式退出防抖定时器 + * + * @returns 无返回值。 + */ + private clearMoveModeExitTimer(): void { + if (this.moveModeExitTimer) { + clearTimeout(this.moveModeExitTimer) + this.moveModeExitTimer = null + } + } + + /** + * 读取 CSS app-region 系统拖拽开关 + * 与设置插件持久化的 settings-general.useCssAppRegionDrag 保持一致。 + * + * @returns 是否已开启 CSS app-region 系统拖拽。 + */ + private isCssAppRegionDragEnabled(): boolean { + try { + const data = databaseAPI.dbGet('settings-general') + return Boolean(data?.useCssAppRegionDrag) + } catch (error) { + console.error('[FloatingBall] 读取 CSS app-region 拖拽配置失败:', error) + return false + } + } + /** * 设置悬浮球启用/禁用 */ @@ -379,13 +505,22 @@ class FloatingBallManager { /** * 销毁悬浮球窗口 + * + * @returns 无返回值。 */ private destroyBallWindow(): void { - if (this.ballWindow && !this.ballWindow.isDestroyed()) { - this.enabled = false // 先标记为禁用,避免 close 事件 preventDefault - // 移除所有事件监听器,防止干扰销毁过程 - this.ballWindow.removeAllListeners('close') - this.ballWindow.destroy() + if (this.ballWindow) { + if (!this.ballWindow.isDestroyed()) { + this.enabled = false // 先标记为禁用,避免 close 事件 preventDefault + // 移除所有事件监听器,防止干扰销毁过程 + this.ballWindow.removeAllListeners('close') + this.ballWindow.removeAllListeners('moved') + this.ballWindow.removeAllListeners('move') + this.ballWindow.destroy() + } + // 复位移动模式并清理退出定时器,避免残留定时器在窗口销毁后触发 + this.moveMode = false + this.clearMoveModeExitTimer() this.ballWindow = null console.log('[FloatingBall] 悬浮球窗口已销毁') } diff --git a/src/main/core/linuxAutoStart.ts b/src/main/core/linuxAutoStart.ts new file mode 100644 index 00000000..3a222f98 --- /dev/null +++ b/src/main/core/linuxAutoStart.ts @@ -0,0 +1,91 @@ +import { app } from 'electron' +import fs from 'fs' +import path from 'path' + +/** XDG autostart 目录相对家目录的路径 */ +const AUTOSTART_DIR_RELATIVE_PATH = path.join('.config', 'autostart') + +/** ZTools 管理的自启桌面条目文件名 */ +const AUTOSTART_FILE_NAME = 'ztools.desktop' + +/** GNOME 启动应用程序面板会把关闭态写成 X-GNOME-Autostart-enabled=false */ +const GNOME_DISABLED_MARKER = 'X-GNOME-Autostart-enabled=false' + +/** 桌面环境通用的禁用标记 */ +const HIDDEN_MARKER = 'Hidden=true' + +/** + * 计算自启桌面条目完整路径。 + * @returns XDG autostart 目录下的 ztools.desktop 绝对路径 + */ +function getAutostartDesktopFile(): string { + return path.join(app.getPath('home'), AUTOSTART_DIR_RELATIVE_PATH, AUTOSTART_FILE_NAME) +} + +/** + * 计算自启桌面条目的 Exec 目标。 + * AppImage 运行时使用 APPIMAGE 环境变量指向的实际镜像;其它打包形式使用可执行文件路径。 + * @returns 加引号后的可执行文件绝对路径 + */ +function getAutostartExecTarget(): string { + const target = process.env.APPIMAGE || app.getPath('exe') + return `"${target}"` +} + +/** + * 生成本次应写入的自启桌面条目内容。 + * @returns UTF-8 桌面条目文本,使用 \n 行尾 + */ +function buildDesktopEntryContent(): string { + return [ + '[Desktop Entry]', + 'Type=Application', + 'Name=ZTools', + `Exec=${getAutostartExecTarget()}`, + 'Terminal=false', + 'StartupNotify=false', + 'X-GNOME-Autostart-enabled=true' + ].join('\n') +} + +/** + * 设置 Linux 开机自启(XDG autostart)。 + * 启用时递归创建 ~/.config/autostart 并写入 ztools.desktop,关闭时删除该文件。 + * @param enable 是否启用开机自启 + * @returns 无返回值 + * @throws 写入或删除自启文件失败时抛出错误,由调用方决定是否回滚开关状态 + */ +export function setLinuxLaunchAtLogin(enable: boolean): void { + const desktopFile = getAutostartDesktopFile() + if (!enable) { + // 关闭时仅删除自启条目,force 使文件不存在时成为无操作。 + fs.rmSync(desktopFile, { force: true }) + return + } + + // 确保 autostart 目录存在,桌面环境只读取该目录下的一级 .desktop 文件。 + fs.mkdirSync(path.dirname(desktopFile), { recursive: true }) + fs.writeFileSync(desktopFile, buildDesktopEntryContent(), { encoding: 'utf8' }) +} + +/** + * 读取 Linux 开机自启状态。 + * 文件缺失视为关闭;存在时被 GNOME 面板关闭或带 Hidden 标记也视为关闭。 + * @returns 自启条目存在且未被禁用时返回 true + * @throws 自启文件存在但读取失败(非 ENOENT)时抛出错误 + */ +export function getLinuxLaunchAtLogin(): boolean { + const desktopFile = getAutostartDesktopFile() + let content: string + try { + content = fs.readFileSync(desktopFile, 'utf8') + } catch (error) { + // 文件未创建过按关闭态处理,其它错误如实上抛以便排查。 + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return false + } + throw error + } + + return !content.includes(GNOME_DISABLED_MARKER) && !content.includes(HIDDEN_MARKER) +} diff --git a/src/main/core/shortcutFallback.ts b/src/main/core/shortcutFallback.ts new file mode 100644 index 00000000..41011847 --- /dev/null +++ b/src/main/core/shortcutFallback.ts @@ -0,0 +1,371 @@ +import { UiohookKey, type UiohookKeyboardEvent } from 'uiohook-napi' +import globalInputManager from './globalInputManager.js' + +const INPUT_CONSUMER = 'shortcut-fallback' + +/** + * Electron accelerator modifier name → uiohook keycode. + */ +const MODIFIER_NAME_TO_KEYCODE: Record = { + Alt: UiohookKey.Alt, + Option: UiohookKey.Alt, // macOS alias + Ctrl: UiohookKey.Ctrl, + Control: UiohookKey.Ctrl, // alias + Shift: UiohookKey.Shift, + Super: UiohookKey.Meta, + Command: UiohookKey.Meta, // alias + Meta: UiohookKey.Meta // alias +} + +/** + * Right-side modifier keycodes → left-side equivalents. + * uiohook-napi emits different keycodes for left and right modifier keys. + * We normalize right-side variants to left-side for matching purposes, + * matching the behavior of doubleTapManager.ts MODIFIER_KEYCODES. + */ +const RIGHT_TO_LEFT_MODIFIER: Record = { + [UiohookKey.AltRight]: UiohookKey.Alt, + [UiohookKey.CtrlRight]: UiohookKey.Ctrl, + [UiohookKey.ShiftRight]: UiohookKey.Shift, + [UiohookKey.MetaRight]: UiohookKey.Meta +} + +/** All modifier keycodes (both left and right) for strict matching check. */ +const ALL_MODIFIER_KEYCODES = new Set([ + ...Object.values(MODIFIER_NAME_TO_KEYCODE), + ...Object.keys(RIGHT_TO_LEFT_MODIFIER).map(Number) +]) + +/** + * Electron accelerator main key name → uiohook keycode. + * Covers letters, digits, function keys, and common special keys. + */ +const KEY_NAME_TO_KEYCODE: Record = { + // Letters + A: UiohookKey.A, + B: UiohookKey.B, + C: UiohookKey.C, + D: UiohookKey.D, + E: UiohookKey.E, + F: UiohookKey.F, + G: UiohookKey.G, + H: UiohookKey.H, + I: UiohookKey.I, + J: UiohookKey.J, + K: UiohookKey.K, + L: UiohookKey.L, + M: UiohookKey.M, + N: UiohookKey.N, + O: UiohookKey.O, + P: UiohookKey.P, + Q: UiohookKey.Q, + R: UiohookKey.R, + S: UiohookKey.S, + T: UiohookKey.T, + U: UiohookKey.U, + V: UiohookKey.V, + W: UiohookKey.W, + X: UiohookKey.X, + Y: UiohookKey.Y, + Z: UiohookKey.Z, + // Digits (top row) + '0': UiohookKey['0'], + '1': UiohookKey['1'], + '2': UiohookKey['2'], + '3': UiohookKey['3'], + '4': UiohookKey['4'], + '5': UiohookKey['5'], + '6': UiohookKey['6'], + '7': UiohookKey['7'], + '8': UiohookKey['8'], + '9': UiohookKey['9'], + // Function keys + F1: UiohookKey.F1, + F2: UiohookKey.F2, + F3: UiohookKey.F3, + F4: UiohookKey.F4, + F5: UiohookKey.F5, + F6: UiohookKey.F6, + F7: UiohookKey.F7, + F8: UiohookKey.F8, + F9: UiohookKey.F9, + F10: UiohookKey.F10, + F11: UiohookKey.F11, + F12: UiohookKey.F12, + // Special keys + Space: UiohookKey.Space, + Tab: UiohookKey.Tab, + Escape: UiohookKey.Escape, + Esc: UiohookKey.Escape, // alias + Backspace: UiohookKey.Backspace, + Enter: UiohookKey.Enter, + Return: UiohookKey.Enter, // alias + Delete: UiohookKey.Delete, + Insert: UiohookKey.Insert, + Home: UiohookKey.Home, + End: UiohookKey.End, + PageUp: UiohookKey.PageUp, + PageDown: UiohookKey.PageDown, + // Arrow keys + Up: UiohookKey.ArrowUp, + Down: UiohookKey.ArrowDown, + Left: UiohookKey.ArrowLeft, + Right: UiohookKey.ArrowRight, + // Symbols + Comma: UiohookKey.Comma, + Period: UiohookKey.Period, + Slash: UiohookKey.Slash, + Semicolon: UiohookKey.Semicolon, + Equal: UiohookKey.Equal, + Minus: UiohookKey.Minus, + Backquote: UiohookKey.Backquote, + BracketLeft: UiohookKey.BracketLeft, + BracketRight: UiohookKey.BracketRight, + Backslash: UiohookKey.Backslash, + Quote: UiohookKey.Quote, + // Numpad + num0: UiohookKey.Numpad0, + num1: UiohookKey.Numpad1, + num2: UiohookKey.Numpad2, + num3: UiohookKey.Numpad3, + num4: UiohookKey.Numpad4, + num5: UiohookKey.Numpad5, + num6: UiohookKey.Numpad6, + num7: UiohookKey.Numpad7, + num8: UiohookKey.Numpad8, + num9: UiohookKey.Numpad9, + numdec: UiohookKey.NumpadDecimal, + numadd: UiohookKey.NumpadAdd, + numsub: UiohookKey.NumpadSubtract, + nummult: UiohookKey.NumpadMultiply, + numdiv: UiohookKey.NumpadDivide, + // PrintScreen + PrintScreen: UiohookKey.PrintScreen +} + +interface ParsedShortcut { + /** Keycodes of required modifiers (must all be pressed) */ + modifierKeycodes: number[] + /** Keycode of the main (non-modifier) key */ + mainKeycode: number +} + +interface ShortcutEntry { + parsed: ParsedShortcut + callback: () => void +} + +/** + * Linux uiohook-based fallback for Electron's globalShortcut. + * + * When globalShortcut.register() fails on Linux (common on X11 due to + * uiohook/XRecord conflicts, or on Wayland where globalShortcut is + * unsupported), this module uses the existing uiohook-napi infrastructure + * to detect key chords and fire callbacks. + * + * Uses the same GlobalInputManager consumer pattern as DoubleTapManager. + */ +class ShortcutFallbackDetector { + /** Registered fallback shortcuts, keyed by accelerator string. */ + private shortcuts = new Map() + + /** Currently pressed keycodes (tracked across keydown/keyup). */ + private pressedKeys = new Set() + + /** Whether listeners have been registered on globalInputManager. */ + private listenersRegistered = false + + // Bound handlers for stable listener references + private boundHandleKeyDown = this.handleKeyDown.bind(this) + private boundHandleKeyUp = this.handleKeyUp.bind(this) + + /** + * Register a fallback shortcut. Returns true on success. + * The accelerator string uses Electron format (e.g. "Alt+Space", "Ctrl+Shift+K"). + */ + register(accelerator: string, callback: () => void): boolean { + const parsed = this.parseAccelerator(accelerator) + if (!parsed) { + console.error(`[ShortcutFallback] 无法解析快捷键: ${accelerator}`) + return false + } + + this.shortcuts.set(accelerator, { parsed, callback }) + this.ensureStarted() + + console.log(`[ShortcutFallback] 注册快捷键: ${accelerator}`) + return true + } + + /** + * Unregister a fallback shortcut. + */ + unregister(accelerator: string): void { + this.shortcuts.delete(accelerator) + this.maybeStop() + } + + /** + * Unregister all fallback shortcuts. + */ + unregisterAll(): void { + this.shortcuts.clear() + this.maybeStop() + } + + /** + * Parse an Electron accelerator string into uiohook modifier + main keycodes. + * Returns null if parsing fails. + * + * Examples: + * "Alt+Space" → { modifierKeycodes: [Alt], mainKeycode: Space } + * "Ctrl+Shift+K" → { modifierKeycodes: [Ctrl, Shift], mainKeycode: K } + * "Super+Escape" → { modifierKeycodes: [Meta], mainKeycode: Escape } + */ + private parseAccelerator(accelerator: string): ParsedShortcut | null { + const parts = accelerator.split('+') + if (parts.length < 2) { + // Single key shortcuts are not supported as global shortcuts + return null + } + + const modifierKeycodes: number[] = [] + let mainKeycode: number | null = null + + for (let i = 0; i < parts.length; i++) { + const part = parts[i] + const isLast = i === parts.length - 1 + + if (!isLast) { + // Treat as modifier + const modKeycode = MODIFIER_NAME_TO_KEYCODE[part] + if (modKeycode === undefined) { + console.error(`[ShortcutFallback] 未知修饰键: ${part} (accelerator: ${accelerator})`) + return null + } + modifierKeycodes.push(modKeycode) + } else { + // Last part is the main key + const keycode = KEY_NAME_TO_KEYCODE[part] + if (keycode === undefined) { + console.error(`[ShortcutFallback] 未知按键: ${part} (accelerator: ${accelerator})`) + return null + } + mainKeycode = keycode + } + } + + if (mainKeycode === null) { + return null + } + + return { modifierKeycodes, mainKeycode } + } + + /** + * Check if a parsed shortcut matches the current set of pressed keys. + * Requires: + * - All required modifier keys are pressed + * - The main key is pressed + * - No extra modifier keys are pressed (strict matching) + */ + private matchShortcut(parsed: ParsedShortcut, pressed: Set): boolean { + // Normalize right-side modifier keys to left-side equivalents + // (e.g., AltRight → Alt, CtrlRight → Ctrl) + const normalized = new Set() + for (const kc of pressed) { + normalized.add(RIGHT_TO_LEFT_MODIFIER[kc] ?? kc) + } + + // Main key must be pressed + if (!normalized.has(parsed.mainKeycode)) { + return false + } + + // All required modifiers must be pressed + for (const modKeycode of parsed.modifierKeycodes) { + if (!normalized.has(modKeycode)) { + return false + } + } + + // No extra modifier keys may be pressed (strict matching) + for (const pressedKeycode of normalized) { + if (pressedKeycode === parsed.mainKeycode) continue + if (parsed.modifierKeycodes.includes(pressedKeycode)) continue + if (ALL_MODIFIER_KEYCODES.has(pressedKeycode)) { + // An extra modifier is held — don't match (e.g., Ctrl held during Alt+Space) + return false + } + } + + return true + } + + private handleKeyDown(e: UiohookKeyboardEvent): void { + this.pressedKeys.add(e.keycode) + + // Debug: log key events periodically to confirm the listener is working + if (this.shortcuts.size > 0) { + const now = Date.now() + if (!this._lastDebugLogTime || now - this._lastDebugLogTime > 3000) { + this._lastDebugLogTime = now + const pressedStr = [...this.pressedKeys].map((k) => `0x${k.toString(16)}`).join(',') + console.log( + `[ShortcutFallback] 监听中,已注册 ${this.shortcuts.size} 个快捷键:`, + [...this.shortcuts.keys()].join(', '), + `当前按下: [${pressedStr}]` + ) + } + } + + // Check all registered shortcuts + for (const [accelerator, entry] of this.shortcuts) { + if (this.matchShortcut(entry.parsed, this.pressedKeys)) { + console.log(`[ShortcutFallback] ✅ 快捷键匹配成功: ${accelerator}`) + // Defer callback to avoid uiohook event-handler reentrancy + // (same pattern as doubleTapManager.fireHandlers) + const cb = entry.callback + setTimeout(() => { + try { + cb() + } catch (error) { + console.error(`[ShortcutFallback] 回调执行失败 (${accelerator}):`, error) + } + }, 0) + } + } + } + + private _lastDebugLogTime: number | undefined + + private handleKeyUp(e: UiohookKeyboardEvent): void { + this.pressedKeys.delete(e.keycode) + } + + private ensureStarted(): void { + if (this.listenersRegistered) return + + this.listenersRegistered = true + globalInputManager.on(INPUT_CONSUMER, 'keydown', this.boundHandleKeyDown) + globalInputManager.on(INPUT_CONSUMER, 'keyup', this.boundHandleKeyUp) + + if (globalInputManager.acquire(INPUT_CONSUMER)) { + console.log('[ShortcutFallback] 全局键盘监听已启动') + } else { + this.listenersRegistered = false + console.error('[ShortcutFallback] 启动全局键盘监听失败') + } + } + + private maybeStop(): void { + if (this.shortcuts.size > 0) return + + globalInputManager.release(INPUT_CONSUMER) + this.listenersRegistered = false + this.pressedKeys.clear() + console.log('[ShortcutFallback] 全局键盘监听已停止(无快捷键注册)') + } +} + +export default new ShortcutFallbackDetector() diff --git a/src/main/managers/windowManager.ts b/src/main/managers/windowManager.ts index 1077e83b..3aebb3ac 100644 --- a/src/main/managers/windowManager.ts +++ b/src/main/managers/windowManager.ts @@ -32,6 +32,11 @@ import pluginManager from './pluginManager' type WindowMaterial = 'mica' | 'acrylic' | 'none' const WINDOW_BLUR_DRAG_INPUT_CONSUMER = 'window-blur-drag' const DEFAULT_MODAL_DIALOG_BLUR_HIDE_RELEASE_DELAY_MS = 500 +// Wayland CSS 拖拽 blur 分类使用的光标轮询与判定阈值 +const CURSOR_POLL_INTERVAL_MS = 100 +const POINTER_MOVING_THRESHOLD_MS = 200 +const LINUX_BLUR_HIDE_CONFIRM_MS = 500 +const DRAG_MOVE_DEFER_MS = 1200 /** * 应用快捷键触发时携带的文件输入 @@ -95,12 +100,17 @@ class WindowManager { private lastFocusTarget: 'mainWindow' | 'plugin' | null = null // 窗口隐藏前的焦点状态 private isRestoringFocus: boolean = false // 是否正在恢复焦点状态(防止 focus 事件监听器干扰) private suppressBlurHide: boolean = false // 临时抑制 blur 事件隐藏窗口(文件关联打开等场景) + private cssAppRegionDragEnabled: boolean = false // Linux 是否启用了 CSS app-region 系统拖拽 // 原生模态对话框关闭前后可能发出排队的 blur/mouseup 事件。 private modalDialogBlurHideSuppressed: boolean = false private modalDialogBlurHideReleaseTimer: ReturnType | null = null private modalDialogBlurHideSuppressionDepth: number = 0 private lastBlurHideTime: number = 0 // blur 导致隐藏窗口的时间戳(用于解决托盘点击竞态) - private blurHideTimer: ReturnType | null = null // Linux blur 延迟隐藏定时器 + private blurHideTimer: ReturnType | null = null // Linux blur 确认隐藏定时器 + private deferredBlurHideTimer: ReturnType | null = null // Linux blur 拖拽移动复核定时器 + private cursorPollTimer: ReturnType | null = null // 窗口可见期间的光标轮询定时器 + private lastCursorPoint: { x: number; y: number } | null = null // 最近一次采样到的全局光标位置 + private lastCursorMoveAt: number = 0 // 最近一次检测到光标移动的时间戳 // Double-tap 唤醒窗口时,Windows 可能紧跟一个短暂 blur;这两个 timer 用于跳过误关闭并补一次焦点。 private doubleTapFocusTimer: ReturnType | null = null private windowsHotkeyFocusTimer: ReturnType | null = null @@ -160,6 +170,167 @@ class WindowManager { } } + /** + * 清除 Linux blur 相关的确认/复核定时器。 + * + * @returns 无返回值。 + */ + private clearLinuxBlurHideTimers(): void { + if (this.blurHideTimer) { + clearTimeout(this.blurHideTimer) + this.blurHideTimer = null + } + if (this.deferredBlurHideTimer) { + clearTimeout(this.deferredBlurHideTimer) + this.deferredBlurHideTimer = null + } + } + + /** + * 更新最近一次光标位置与移动时间。 + * + * 窗口可见期间由轮询定时器调用;blur 分类前也会主动调用一次, + * 避免基于过期位置做判断。 + * + * @returns 无返回值。 + */ + private updateCursorPoint(): void { + const point = screen.getCursorScreenPoint() + if ( + this.lastCursorPoint && + (point.x !== this.lastCursorPoint.x || point.y !== this.lastCursorPoint.y) + ) { + // 检测到位置变化时记录移动时间,供 blur 瞬间判断光标是否正在移动。 + this.lastCursorMoveAt = Date.now() + } + this.lastCursorPoint = point + } + + /** + * 开始轮询全局光标位置。 + * + * 仅在窗口可见期间轮询,窗口隐藏后无需继续采样。 + * + * @returns 无返回值。 + */ + private startCursorPolling(): void { + if (this.cursorPollTimer) return + + // 重置历史位置后立即采样一次,保证 blur 分类有可用的光标基线。 + this.lastCursorPoint = null + this.lastCursorMoveAt = 0 + this.updateCursorPoint() + this.cursorPollTimer = setInterval(() => { + this.updateCursorPoint() + }, CURSOR_POLL_INTERVAL_MS) + } + + /** + * 停止轮询全局光标位置并清理相关定时器。 + * + * 窗口隐藏或销毁时调用,避免隐藏后定时器继续触发。 + * + * @returns 无返回值。 + */ + private stopCursorPolling(): void { + if (this.cursorPollTimer) { + clearInterval(this.cursorPollTimer) + this.cursorPollTimer = null + } + this.lastCursorPoint = null + this.lastCursorMoveAt = 0 + this.clearLinuxBlurHideTimers() + } + + /** + * 判断光标是否在阈值时间内发生过移动。 + * + * @returns 光标在 POINTER_MOVING_THRESHOLD_MS 内有移动时返回 true。 + */ + private isPointerMoving(): boolean { + if (!this.lastCursorPoint) return false + return Date.now() - this.lastCursorMoveAt <= POINTER_MOVING_THRESHOLD_MS + } + + /** + * 启动 Linux blur 确认隐藏定时器。 + * + * 用于“光标在窗口外且静止”的常规外部点击场景,确认窗口确实失焦后再隐藏。 + * + * @returns 无返回值。 + */ + private scheduleLinuxBlurHideConfirm(): void { + this.clearLinuxBlurHideTimers() + this.blurHideTimer = setTimeout(() => { + this.blurHideTimer = null + if (this.isBlurHideSuppressed()) return + // 主窗口重新获焦 → 不隐藏 + if (this.mainWindow?.isFocused()) return + // 插件视图持有焦点(应用内部切换)→ 不隐藏 + if (pluginManager.isPluginViewFocused()) return + // 确认是点击了其他窗口,隐藏 + this.lastBlurHideTime = Date.now() + this.hideWindow(false) + }, LINUX_BLUR_HIDE_CONFIRM_MS) + } + + /** + * 启动/顺延 Linux blur 拖拽移动复核定时器。 + * + * 光标在窗口外且仍在移动时使用;复核时重新分类,避免误隐藏正在进行的系统拖拽。 + * + * @returns 无返回值。 + */ + private scheduleLinuxBlurHideRecheck(): void { + this.clearLinuxBlurHideTimers() + this.deferredBlurHideTimer = setTimeout(() => { + this.deferredBlurHideTimer = null + this.classifyLinuxBlurHide(true) + }, DRAG_MOVE_DEFER_MS) + } + + /** + * 分类处理 Linux 下窗口 blur 是内部场景还是点击外部。 + * + * 拖拽起步的 blur 发生时指针仍停在按下点(窗口内),点击其他窗口时指针必然在窗口外, + * 因此以 blur/复核瞬间的光标位置与移动状态区分两者。 + * + * @param isDeferredRecheck 是否由复核定时器触发;复核确认静止后直接隐藏。 + * @returns 无返回值。 + */ + private classifyLinuxBlurHide(isDeferredRecheck: boolean): void { + if (this.isBlurHideSuppressed()) return + + // 先刷新光标快照,避免窗口移动后基于过期 bounds 判断。 + this.updateCursorPoint() + + if (this.lastCursorPoint && this.isPointInsideMainWindow(this.lastCursorPoint)) { + // 指针仍在窗口内:拖拽起步、插件视图获焦等内部场景,不启动隐藏定时器。 + console.log('[Window] Linux blur:光标位于窗口内(疑似 CSS 拖拽起步/插件获焦),跳过隐藏') + this.clearLinuxBlurHideTimers() + return + } + + if (this.isPointerMoving()) { + // 指针在窗口外且仍在移动:疑似拖拽进行中,延后到移动停止后再判定。 + console.log('[Window] Linux blur:光标在窗口外且正在移动(疑似拖拽进行中),延后复核') + this.scheduleLinuxBlurHideRecheck() + return + } + + console.log('[Window] Linux blur:光标在窗口外且静止(疑似点击其他窗口),准备隐藏') + if (isDeferredRecheck) { + // 复核时指针已静止且仍失焦,直接隐藏,不再额外等待确认定时器。 + if (this.mainWindow?.isFocused()) return + if (pluginManager.isPluginViewFocused()) return + this.lastBlurHideTime = Date.now() + this.hideWindow(false) + return + } + + this.scheduleLinuxBlurHideConfirm() + } + /** * 判断当前是否应阻止失焦自动隐藏,E2E 模式始终保持窗口可见。 * @@ -486,6 +657,18 @@ class WindowManager { this.mainWindow.on('blur', () => { if (this.isBlurHideSuppressed()) return + // 插件视图激活且开启 Wayland 原生 CSS 拖拽时,blur 无法与“点击外部”可靠区分 + // (拖拽停顿期间 bounds 可能已失效),此时跳过 blur 自动隐藏,避免拖动插件窗口被误关。 + // 主搜索界面不使用 CSS 拖拽,仍按光标位置分类外部点击并隐藏窗口。 + if ( + platform.isLinux && + this.cssAppRegionDragEnabled && + pluginManager.getCurrentPluginPath() !== null + ) { + console.log('[Window] Linux 已开启 CSS app-region 拖拽且插件视图激活,跳过 blur 自动隐藏') + return + } + // 左键仍按下时可能是从外部拖文件进窗口,先等 mouseup 再决定是否隐藏。 if (this.leftMouseDown) { this.deferBlurHideUntilMouseUp() @@ -493,23 +676,9 @@ class WindowManager { } if (platform.isLinux) { - // Linux 上去掉了 type:'panel',现在 blur 只会在真正点击其他窗口时触发。 - // 但插件 WebContentsView 获焦仍会触发 blur,需延迟排除。 - if (this.blurHideTimer) { - clearTimeout(this.blurHideTimer) - this.blurHideTimer = null - } - this.blurHideTimer = setTimeout(() => { - this.blurHideTimer = null - if (this.isBlurHideSuppressed()) return - // 主窗口重新获焦 → 不隐藏 - if (this.mainWindow?.isFocused()) return - // 插件视图持有焦点(应用内部切换)→ 不隐藏 - if (pluginManager.isPluginViewFocused()) return - // 确认是点击了其他窗口,隐藏 - this.lastBlurHideTime = Date.now() - this.hideWindow(false) - }, 150) + // Linux(Wayland/X11 拖拽)在 blur 触发瞬间用光标位置与移动状态分类, + // 不再依赖延迟后可能已失效的窗口 bounds。 + this.classifyLinuxBlurHide(false) } else { // macOS / Windows:原有行为不变 this.lastBlurHideTime = Date.now() @@ -523,6 +692,9 @@ class WindowManager { } this.mainWindow.on('show', () => { + // 窗口显示期间持续追踪光标位置,供 blur 触发瞬间判断拖拽/点击外部。 + this.startCursorPolling() + // 开始恢复焦点流程,防止 focus 事件监听器修改 lastFocusTarget this.isRestoringFocus = true const savedFocusTarget = this.lastFocusTarget @@ -544,6 +716,15 @@ class WindowManager { this.isRestoringFocus = false }) + this.mainWindow.on('hide', () => { + // 窗口隐藏后停止光标轮询,避免隐藏期间继续采样并残留 blur 定时器。 + this.stopCursorPolling() + }) + + this.mainWindow.on('closed', () => { + this.stopCursorPolling() + }) + // 阻止窗口被销毁(Command+W 时隐藏而不是关闭) this.mainWindow.on('close', (event) => { if (process.env.ZTOOLS_E2E !== '1' && !this.isQuitting) { @@ -569,6 +750,7 @@ class WindowManager { // 从数据库加载唤醒黑名单 const initSettings = databaseAPI.dbGet('settings-general') + this.cssAppRegionDragEnabled = Boolean(initSettings?.useCssAppRegionDrag) if (initSettings?.wakeupBlacklist) { this.wakeupBlacklist = initSettings.wakeupBlacklist } @@ -1155,6 +1337,21 @@ class WindowManager { console.log('[Window] 更新窗口呼出位置策略:', strategy) } + /** + * 更新 Linux CSS app-region 拖拽开关状态。 + * + * @param enabled 是否启用 CSS app-region 系统拖拽。 + * @returns 无返回值。 + */ + public setCssAppRegionDragEnabled(enabled: boolean): void { + this.cssAppRegionDragEnabled = enabled + // 启用 CSS 系统拖拽后不再使用 blur 自动隐藏,清掉可能残留的确认/复核定时器。 + if (enabled) { + this.clearLinuxBlurHideTimers() + } + console.log('[Window] CSS app-region 拖拽:', enabled ? '已启用' : '已禁用') + } + /** * 获取打开窗口前激活的窗口 */ diff --git a/src/preload/index.ts b/src/preload/index.ts index b7f3e555..9f747c4f 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -311,6 +311,18 @@ const api = { ) => { ipcRenderer.on('update-acrylic-opacity', (_event, data) => callback(data)) }, + /** + * 监听 CSS app-region 拖拽开关的实时更新。 + * @param callback 接收开关状态(true 表示由系统接管搜索框区域拖拽)的回调函数 + * @returns 用于移除监听器的清理函数 + */ + onUpdateCssAppRegionDrag: (callback: (enabled: boolean) => void): (() => void) => { + const handler = (_event: Electron.IpcRendererEvent, enabled: boolean): void => callback(enabled) + ipcRenderer.on('update-css-app-region-drag', handler) + return (): void => { + ipcRenderer.removeListener('update-css-app-region-drag', handler) + } + }, onIpcLaunch: ( callback: (options: { path: string diff --git a/src/renderer/src/components/search/SearchBox.vue b/src/renderer/src/components/search/SearchBox.vue index f34873b9..465e94ab 100644 --- a/src/renderer/src/components/search/SearchBox.vue +++ b/src/renderer/src/components/search/SearchBox.vue @@ -2,6 +2,7 @@