feat: 在 linux 下使用 portal 注册全局快捷键 - #162
Conversation
86cc6ce to
fbebc4a
Compare
There was a problem hiding this comment.
Pull request overview
为 Linux 增加基于 XDG Desktop Portal 的全局快捷键,并在不可用时回退 Electron 实现。
Changes:
- 新增 Rust
linux-portal原生模块及 portal 会话管理。 - 扩展快捷键 IPC、状态管理和设置界面。
- 补充构建配置、类型、国际化与 Wayland 文档。
Reviewed changes
Copilot reviewed 29 out of 32 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
tsconfig.node.json |
添加原生模块别名 |
src/stores/hotkey.ts |
管理 portal 模式状态 |
src/settings/categories/hotkeys.ts |
添加 portal 开关 |
src/main.ts |
同步本地化快捷键描述 |
src/i18n/locales/zh-CN.json |
添加中文文案 |
src/i18n/locales/en-US.json |
添加英文文案并调整 Orpheus 文案 |
src/components/settings/custom/HotkeyConfig.vue |
添加 portal 模式界面 |
shared/utils/accelerator.ts |
转换 XDG 触发器格式 |
shared/types/settings.ts |
扩展系统配置类型 |
shared/types/hotkey.ts |
扩展快捷键 API 类型 |
shared/defaults/settings.ts |
默认启用 portal |
scripts/build-native.ts |
Linux 下构建新模块 |
native/linux-portal/src/shortcuts.rs |
实现 portal 会话及绑定 |
native/linux-portal/src/model.rs |
定义 NAPI 数据模型 |
native/linux-portal/src/logger.rs |
实现原生日志 |
native/linux-portal/src/lib.rs |
暴露 NAPI 接口 |
native/linux-portal/package.json |
配置 NAPI 构建 |
native/linux-portal/index.d.ts |
生成原生模块类型 |
native/linux-portal/Cargo.toml |
配置 Rust 依赖 |
native/linux-portal/build.rs |
初始化 NAPI 构建 |
electron/preload/index.ts |
暴露 portal IPC |
electron/main/utils/nativeLoader.ts |
调整原生模块加载路径 |
electron/main/services/globalHotkey.ts |
实现双后端快捷键服务 |
electron/main/ipc/hotkey.ts |
注册 portal IPC |
electron/main/ipc/config.ts |
响应 portal 配置变更 |
electron.vite.config.ts |
添加 Vite 路径别名 |
electron-builder.config.ts |
打包 portal 二进制 |
docs/troubleshooting/wayland.md |
更新中文 Wayland 指南 |
docs/en/troubleshooting/wayland.md |
更新英文 Wayland 指南 |
components.d.ts |
更新自动生成组件声明 |
Cargo.toml |
加入 workspace |
Cargo.lock |
锁定新增依赖 |
Suppressed comments (1)
electron/main/services/globalHotkey.ts:233
- 必修:快速执行“关闭 portal → 再开启”时,关闭操作会递增
portalEpoch,但再次开启仍复用旧的readyPromise。旧初始化随后因 epoch 不匹配退出,finally只清空 promise,不会按最新配置重新初始化,最终设置显示开启但模式仍是 Electron。请记录最新期望模式,并在旧任务结束后为新代际重新运行初始化。
const runPortalReady = (): Promise<void> => {
if (!readyPromise) {
readyPromise = ensurePortalReady().finally(() => {
readyPromise = null;
});
}
return readyPromise;
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
e75e7ac to
1308f40
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 30 changed files in this pull request and generated 2 comments.
Suppressed comments (4)
electron/main/services/globalHotkey.ts:225
- 必修:能力探测失败后
portalCapability会缓存一个supported: false的对象;用户之后关闭再开启 portal 时,这个if被跳过,代码会继续进入绑定流程,而不是保持 Electron 回退。请把supported判断移到缓存判断之外(或失败时清空缓存),确保缓存的“不支持”结果也会直接回退。
if (!portalCapability) {
portalCapability = await portalModule.detect();
if (epoch !== portalEpoch) return;
if (!portalCapability.supported) {
electron/main/services/globalHotkey.ts:259
- 必修(并发):这里正在
enqueueTransition的回调内部await switchToElectron(),而后者会再次排入同一个transitionChain。新节点必须等当前节点结束,当前节点又在等待新节点,因此首次 portal 绑定失败时会永久死锁,readyPromise/getGlobalMode也不会完成。应在当前节点内直接完成回退,或抽出一个不再入队的内部切换函数。
if (!res.ok) {
coreLog.error(`[portal] 绑定全局快捷键失败: ${res.error ?? "未知错误"}`);
await switchToElectron();
return;
electron/main/services/globalHotkey.ts:219
- 必修(并发):portal 信号被无条件转发。关闭全局快捷键时
unbindShortcuts()是异步排队的,旧会话在关闭完成前仍可能发出 Activated;切回 Electron 时也可能收到已排队的旧信号,导致禁用后仍执行动作或切换期间重复触发。回调应按当前模式和portalBound丢弃失效事件。
This issue also appears on line 256 of the same file.
portalModule.onActivated((_err, id) => {
broadcast("hotkey:trigger", id);
});
native/linux-portal/src/shortcuts.rs:261
- 必修(并发):这里持有
activated_cb的互斥锁调用外部 ThreadsafeFunction。即使当前使用 NonBlocking,调用路径仍不应占用共享状态锁,否则回调替换/关闭会被阻塞,未来调用语义变化时也可能形成重入死锁。请先克隆回调并释放 guard,再执行call。
let cb = state()
.activated_cb
.lock()
.unwrap_or_else(|e| e.into_inner());
if let Some(cb) = cb.as_ref() {
| const res = await portalModule.bindShortcuts(buildPortalShortcuts()); | ||
| if (!res.ok) { | ||
| coreLog.error(`[portal] 绑定全局快捷键失败: ${res.error ?? "未知错误"}`); | ||
| } | ||
| }); |
| Err(AshpdError::Response(ResponseError::Cancelled)) => { | ||
| // 用户取消授权框:快捷键已注册,仅未分配 preferred_trigger,保留会话 | ||
| info!("用户取消全局快捷键授权,快捷键仍注册成功(未分配默认触发器)"); | ||
| Vec::new() | ||
| } |
11d4630 to
6e74ea4
Compare
主要是为了在 linux 下打包方式导致的原生模块路径差异。顺便也把开发环境下的原生模块路径也换成了 `app.getAppPath()`
|
我对splayer-next代码库不了解 |
我知道有这个东西,文档里面我也写了关于它的一段。能用,但很灵车(难用我没在 SPlayer-Next 的文档说,我不确定是不是只有我一个人这样,我没搞清它如何工作🤔)。 |
|
看了下electron有数个相关issue没有解决,也许可以等这个pr合并... |
改动类型
是否包含破坏性变更
改动说明
在 Linux 下使用 xdg-desktop-portal 注册全局快捷键
测试情况
包含巨量 AI 生成代码,已经人类初步审阅
已在 KDE Plasma Wayland 下测试通过,测试了使用新增的 portal 模式,和关闭
portalGlobalShortcuts回退的 electron 模式(非 Linux 系统下回退或在 Linux 下手动回退)自查清单
pnpm format,并确认pnpm typecheck、pnpm lint通过pnpm build:native验证;未手写native/*/index.d.tsdev分支提交