impress_voice_input/gnome-extension/io.impress.voice-input-hotkey@impress/extension.js
impressionyang 20bc24a028 feat(linux): 新增多后端快捷键支持和录音指示器
- XCB 后端:X11 下 XGrabKey 零权限全局快捷键
- evdev 后端:Wayland 下 EVIOCGRAB + uinput 键盘事件拦截与重放
- GNOME 扩展 D-Bus 后端:GNOME Wayland 备用方案
- Portal GlobalShortcuts 后端:标准 Portal 快捷键
- libei 后端:GNOME 47+/KDE 实验性支持
- 后端工厂:根据会话类型和权限自动选择最优后端
- 录音指示器:浮动窗口在光标位置显示录音状态

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-15 16:43:16 +08:00

126 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import Clutter from 'gi://Clutter';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
const DBUS_IFACE = `
<node>
<interface name="io.impress.VoiceInputHotkey">
<signal name="CapsLockPressed"/>
<signal name="CapsLockReleased"/>
</interface>
</node>`;
const DBUS_PATH = '/io/impress/VoiceInputHotkey';
const CAPS_LOCK_KEYSYM = 0xffe5;
export default class VoiceInputHotkeyExtension extends Extension {
constructor(metadata) {
super(metadata);
this._dbusNode = null;
this._captorId = 0;
this._releaseCaptorId = 0;
this._retryId = 0;
}
enable() {
try {
this._dbusNode = Gio.DBusExportedObject.wrapJSObject(DBUS_IFACE, {});
this._dbusNode.export(Gio.DBus.session, DBUS_PATH);
this.log('D-Bus 已导出');
this._tryConnect();
} catch (e) {
this.log('enable 失败: ' + e.message);
}
}
_tryConnect() {
if (this._retryId) {
GLib.Source.remove(this._retryId);
this._retryId = 0;
}
// 优先使用 uiGroup始终可用回退到 global.stage
let target = null;
try {
target = Main.layoutManager?.uiGroup || null;
} catch (e) {}
if (!target) {
try { target = global.stage || null; } catch (e) {}
}
if (!target) {
this.log('uiGroup 和 stage 均不可用200ms 后重试');
this._retryId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 200, () => {
this._retryId = 0;
this._tryConnect();
return GLib.SOURCE_REMOVE;
});
return;
}
try {
this._captorId = target.connect('captured-event::key-press', (_actor, event) => {
if (event.type() !== Clutter.EventType.KEY_PRESS) return Clutter.EVENT_PROPAGATE;
const sym = event.get_key_symbol();
if (sym === CAPS_LOCK_KEYSYM || sym === Clutter.KEY_Caps_Lock) {
this._dbusNode.emit_signal('CapsLockPressed', null);
this.log('CapsLock pressed');
}
return Clutter.EVENT_PROPAGATE;
});
this._releaseCaptorId = target.connect('captured-event::key-release', (_actor, event) => {
if (event.type() !== Clutter.EventType.KEY_RELEASE) return Clutter.EVENT_PROPAGATE;
const sym = event.get_key_symbol();
if (sym === CAPS_LOCK_KEYSYM || sym === Clutter.KEY_Caps_Lock) {
this._dbusNode.emit_signal('CapsLockReleased', null);
this.log('CapsLock released');
}
return Clutter.EVENT_PROPAGATE;
});
this.log('全局按键捕获已就绪');
} catch (e) {
this.log('连接事件失败: ' + e.message);
this._retryId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => {
this._retryId = 0;
this._tryConnect();
return GLib.SOURCE_REMOVE;
});
}
}
disable() {
try {
if (this._retryId) {
GLib.Source.remove(this._retryId);
this._retryId = 0;
}
// 从所有可能的目标断开
let targets = [];
try { if (Main.layoutManager?.uiGroup) targets.push(Main.layoutManager.uiGroup); } catch (e) {}
try { if (global.stage) targets.push(global.stage); } catch (e) {}
for (const t of targets) {
if (this._captorId) { try { t.disconnect(this._captorId); } catch (e) {} }
if (this._releaseCaptorId) { try { t.disconnect(this._releaseCaptorId); } catch (e) {} }
}
this._captorId = 0;
this._releaseCaptorId = 0;
if (this._dbusNode) { this._dbusNode.unexport(); this._dbusNode = null; }
this.log('Extension disabled');
} catch (e) {
this.log('disable 异常: ' + e.message);
}
}
log(msg) {
console.log(`[ImpressHotkey] ${msg}`);
}
}