WaylandKeybinds: reliable Discord global keybinds on GNOME Wayland
Vencord plugin for the Discord desktop app. Toggles fire on key press, push-to-talk infers releases GNOME drops (re-registration probe), and GNOME's trigger descriptions are matched correctly. Includes basis-path tests, a patch checker against the live Discord bundle, the design spec and the README assets.
This commit is contained in:
commit
a3a9149184
10 files changed
+1514
No files matched your search
@@ -0,0 +1,108 @@
|
||||
# WaylandKeybinds: atalhos globais confiáveis do Discord no GNOME (Wayland)
|
||||
**Autor:** Evandro · **Data:** 2026-11-01 · **Porte:** médio · **Status:** concluída
|
||||
|
||||
## 1. Objetivo
|
||||
Fazer os atalhos globais do Discord (alternar mudo/ensurdecer, push-to-talk) funcionarem de forma confiável no GNOME Wayland para quem usa Vencord. O Discord 1.0.168 já registra atalhos pelo portal `GlobalShortcuts`, mas no GNOME o evento de soltar falha e o estado do portal é lido errado.
|
||||
|
||||
## 2. Escopo
|
||||
- Dentro: plugin de renderer `src/plugins/waylandKeybinds.discordDesktop/` (sem `native.ts`, sem dependência nova, sem DOM), com lógica pura testável e dois patches no módulo do `KeybindsStore`.
|
||||
- Fora: push-to-talk perfeito com modificadores (o mutter não entrega a soltura; ver §6), botões de mouse, `SOUNDBOARD_HOLD` (o Discord desliga o portal para ele no Linux), atalhos do VoiceGuard, Vesktop, PR upstream.
|
||||
|
||||
## 3. Critérios de aceite
|
||||
- CA1: Com portal ativo e atalho representável, uma ação só-soltar (`TOGGLE_MUTE`, `TOGGLE_DEAFEN` etc.) dispara no pressionar, também quando o GNOME perde a soltura, e o gesto seguinte não é engolido. → `wk:test` + teste ao vivo T8.
|
||||
- CA2: Ação de segurar (push-to-talk): termina na soltura real ou, se ela se perder, em até ~1,3 s depois de a tecla ser solta (sem repetição após re-registro); continua enquanto a repetição chegar. → `wk:test` (relógio falso).
|
||||
- CA9: Quando o Discord desregistra o atalho (apagado/alterado), a sondagem para e nada é re-registrado. → `wk:test` (`cancel`) + patch do `inputEventUnregister` no CA5.
|
||||
- CA3: Sem portal (inativo, combinação não representável ou `portal:false`) o registro passa intacto: mesmo callback, mesmas opções. → `pnpm wk:test`.
|
||||
- CA4: A descrição do GNOME `Press <Shift><Control>F12` é reconhecida como igual a `CTRL+SHIFT+F12`; o estado passa a `matchesTrigger: true`. Estados diferentes, não vinculados ou sem gatilho esperado ficam como estão. → `pnpm wk:test`.
|
||||
- CA5: Os patches aplicam no Discord atual: cada `find` ocorre uma vez, cada `match` casa uma vez e o bundle corrigido continua válido. → `node check-patches.mjs` contra `discord.com/app` baixado.
|
||||
- CA6: `pnpm build --dev`, `tsc --noEmit` e `eslint` do plugin sem erros. → comandos.
|
||||
- CA7: Toda função de `logic.ts` com CC ≤ 10 e todos os caminhos-base cobertos. → `uvx lizard` + tabela na seção 8.
|
||||
- CA8: No Discord real, alternar mudo por atalho global funciona soltando os modificadores antes da tecla. → teste ao vivo com o usuário (exige reiniciar o Discord).
|
||||
|
||||
## 4. Contexto verificado
|
||||
- `modules/discord_utils-1/discord_utils/discord_utils.node`: `WaylandGlobalShortcuts` (sd-bus), log `Wayland GlobalShortcuts initialized; appId='discord'` em toda inicialização (journal); filtra eventos por `keydown`/`keyup` (medido: atalho só-soltar não entrega o pressionar).
|
||||
- Renderer `web.f86c748842ee176f.js`, módulo do `KeybindsStore`: `inputEventRegister(e,t,n,i)` com `{focused:!0,blurred:!0,keydown:!1,keyup:!1,...keyEvents,description,shortcutId}`; `setWaylandPortalShortcutsCallback(e=>{...t[String(n.eventId)]=n;...})`.
|
||||
- Tabela de ações: `TOGGLE_MUTE`/`TOGGLE_DEAFEN` = `keyup:!0,keydown:!1`; `PUSH_TO_TALK` = `keyup:!0,keydown:!0`; `SOUNDBOARD_HOLD` = `portal:isWindows()`.
|
||||
- Tela de atalhos (`82738a067c897563.js`, componente `lB`): `bound && matchesTrigger===false` mostra aviso com `triggerDescription`; regravar a mesma combinação chama `reofferWaylandPortalShortcut` (reabre o diálogo do GNOME).
|
||||
- mutter `src/core/keybindings.c` `process_event`: o soltar procura o atalho pelo código da tecla + modificadores ainda pressionados; soltar um modificador antes faz o soltar nunca casar.
|
||||
- Portal do GNOME 50 devolve `triggerDescription` = `Press <Shift><Control>F12`; o nativo compara com `CTRL+SHIFT+F12` (de `getWaylandPortalTrigger`) e marca `matchesTrigger:false` sempre.
|
||||
- Padrão de plugin: `src/plugins/noDeepLinks.web/index.ts` (patch com `$self`).
|
||||
|
||||
## 5. Desenho
|
||||
- Corrigir no renderer, onde o Discord monta o registro: troca o modo de entrega por ação. Alternativas descartadas: D-Bus próprio (`dbus-next`/Rust: rejeitado nos PRs #326/#3505 e duplica o que o nativo já faz) e clique em DOM (regra do CONTRIBUTING).
|
||||
- `logic.ts` (puro, sem imports do Vencord):
|
||||
- `type DispatchMode = "passthrough" | "pressAsRelease" | "hold"`
|
||||
- `planRegistration(options: KeyEventOptions, portalActive: boolean, representable: boolean): DispatchMode`
|
||||
- `adjustOptions<T extends KeyEventOptions>(options: T, mode: DispatchMode): T` (pede pressionar e soltar ao nativo)
|
||||
- `createDispatcher(callback, mode, rearm: () => void, scheduler: Scheduler): { dispatch, cancel }`: no pressionar age e agenda sondagem em `FIRST_PROBE_MS` (1000); sondar = `rearm()` (re-registro, zera o estado preso do nativo) + janela `PROBE_WINDOW_MS` (300); repetição em até `ECHO_MS` (150) após o re-registro = ainda segurado; sem repetição = solto; soltura dentro de `ECHO_MS` é eco sintético do re-registro e é ignorada.
|
||||
- `normalizeTrigger(trigger: string): string` → forma canônica `MODS|tecla`
|
||||
- `fixPortalState<S extends PortalState>(state: S, expected: string | undefined): S`
|
||||
- `index.ts`: três patches no `find: "Skipping registration."`: `$self.wrapRegistration(bridge, id, shortcut, callback, options)` (devolve `[callback, options]`; o `rearm` usa a própria ponte do Discord), `$self.unregister(bridge, id)` (cancela a sondagem antes de desregistrar) e `$self.fixPortalState(state)`. Entradas vêm do nativo (`isWaylandGlobalShortcutsAvailable`, `getWaylandPortalTrigger`) e da store (`getUserAgnosticState()[id].shortcut`), tudo em `try/catch` com fallback para o comportamento original.
|
||||
- `patches.ts`: dados dos patches, usados pelo `index.ts` e pelo `check-patches.mjs`.
|
||||
- Arquivos (todos novos): `logic.ts`, `logic.test.ts`, `patches.ts`, `index.ts`, `check-patches.mjs`, esta spec.
|
||||
|
||||
## 6. Restrições, riscos, suposições
|
||||
- A inferência de soltura depende da repetição de tecla do GNOME (medido: atraso 500 ms, intervalo 30 ms). Com repetição desligada, o push-to-talk terminaria ~1,3 s após o pressionar mesmo segurado; com atraso > 1000 ms, a primeira sondagem erra. Reverter: desligar o plugin.
|
||||
- Cada sondagem é um re-registro (D-Bus `BindShortcuts`); só ocorre enquanto a tecla está pressionada. Medido: não reabre o diálogo do GNOME.
|
||||
- Patches dependem do bundle minificado; quebram em atualizações do Discord (o CA5 detecta).
|
||||
- Explicado na v2: os "pressionar" extras e os gestos sumidos vinham da repetição automática do GNOME e do estado preso do nativo.
|
||||
|
||||
## 7. Microtasks
|
||||
### T1 · escrever testes de caminhos-base de `logic.ts` · N0
|
||||
- Serve: CA1, CA2, CA3, CA4, CA7 · Depende de: nenhuma
|
||||
- Arquivos: criar `logic.test.ts`
|
||||
- Pronto quando: um caso por caminho-base de cada função; roda e falha (lógica ausente)
|
||||
- Verificar: `pnpm wk:test` → falha de import/asserção
|
||||
- [x] feito
|
||||
### T2 · implementar `logic.ts` · N0
|
||||
- Serve: CA1–CA4 · Depende de: T1
|
||||
- Pronto quando: todos os testes passam
|
||||
- Verificar: `pnpm wk:test` → 0 falhas
|
||||
- [x] feito
|
||||
### T3 · medir CC e conferir caminhos · N0
|
||||
- Serve: CA7 · Depende de: T2
|
||||
- Verificar: `uvx lizard logic.ts` → CC ≤ 10 em todas
|
||||
- [x] feito
|
||||
### CP1 · após T1–T3 · N0
|
||||
- [x] feito
|
||||
### T4 · `patches.ts` + `index.ts` · N0
|
||||
- Serve: CA1, CA4, CA6 · Depende de: T2
|
||||
- Padrão: `src/plugins/noDeepLinks.web/index.ts`
|
||||
- Verificar: `pnpm build --dev` → sucesso
|
||||
- [x] feito
|
||||
### T5 · `check-patches.mjs` contra o bundle real · N0
|
||||
- Serve: CA5 · Depende de: T4
|
||||
- Verificar: `node check-patches.mjs` → find único, match único, bundle válido
|
||||
- [x] feito
|
||||
### T6 · tsc + eslint · N0
|
||||
- Serve: CA6 · Depende de: T4
|
||||
- [x] feito
|
||||
### CP2 · após T4–T6 · N0
|
||||
- [x] feito
|
||||
### T7 · teste real: nativo do Discord + GNOME + `logic.ts` · N0
|
||||
- Serve: CA1, S1 · Depende de: T2 · precisa de um toque do usuário
|
||||
- [x] feito
|
||||
### T8 · teste ao vivo no Discord · N0
|
||||
- Serve: CA8 · Depende de: CP2 · precisa do ok do usuário para reiniciar o Discord
|
||||
- [x] feito
|
||||
|
||||
## 8. Resultado por critério
|
||||
| CA | Resultado | Evidência |
|
||||
|---|---|---|
|
||||
| CA1 | ok | 31/31 testes; ao vivo: 11 pressionamentos → 11 alternâncias de mudo (11–16 ms), inclusive segurar de 6 s soltando os modificadores antes (57 repetições do GNOME → 1 alternância) e os gestos seguintes |
|
||||
| CA2 | ok (unidade) | testes com relógio falso; push-to-talk não exercitado ao vivo (sem atalho cadastrado) |
|
||||
| CA3 | ok | testes |
|
||||
| CA4 | ok | testes; ao vivo `Press <Shift><Control>F10/F11` → `matchesTrigger: true` |
|
||||
| CA5 | ok | `check-patches.mjs`: find único (módulo 532624), 3 matches × 1, módulo válido; ao vivo `Patched by WaylandKeybinds` |
|
||||
| CA6 | ok | `pnpm build --dev`, `tsc` 0 erros, eslint 0 |
|
||||
| CA7 | ok | CC máx. 7 (`planRegistration`), despachante 6; 32/32 mutantes mortos |
|
||||
| CA8 | ok (mudo) | confirmado pelo usuário; ensurdecer registrado e vinculado no GNOME, não pressionado no teste |
|
||||
| CA9 | ok (unidade) | teste `cancel` + patch do `inputEventUnregister` aplicado; não exercitado ao vivo |
|
||||
|
||||
## 9. Registro
|
||||
- 2026-11-01 — aprovação prévia do usuário ("pode fazer tudo de maneira autônoma"); portão da spec dispensado.
|
||||
- 2026-11-01 — `wk:test` é um alias de comando, não script no `package.json` (não mexer em arquivo versionado): `pnpm exec esbuild logic.test.ts --bundle --platform=node --format=esm --outfile=/tmp/wk-test.mjs && node --test /tmp/wk-test.mjs`.
|
||||
- 2026-11-02 — v1 (agir no pressionar) insuficiente ao vivo: o nativo do Discord ignora `Activated` enquanto acha a tecla pressionada; soltura perdida o deixa preso e o gesto seguinte some. Medido com `busctl monitor` (portal) × eventos crus no renderer (CDP): repetição automática do GNOME (500 ms, depois 30 ms) e engolimento até `Deactivated`.
|
||||
- 2026-11-02 — experimento ao vivo: re-registrar (`inputEventUnregister`+`inputEventRegister`) zera o estado; com a tecla segurada, a repetição chega 0–30 ms após o re-registro; solta, nada chega. Base da v2 (sondagem). Desregistro gera soltura sintética assíncrona (tratada como eco).
|
||||
- 2026-11-02 — usuário pediu tecla configurável: já é, pela tela de atalhos do Discord; o plugin não fixa teclas e vale para todas as ações (mudo, ensurdecer, push-to-talk).
|
||||
- 2026-11-02 — a pedido do usuário, movido de `src/userplugins` para `src/plugins` (branch `feat/wayland-keybinds`, sem commit) com `enabledByDefault: true`: só altera o comportamento quando o Discord registra pelo portal.
|
||||
Reference in new issue
Block a user