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.
10 KiB
10 KiB
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/(semnative.ts, sem dependência nova, sem DOM), com lógica pura testável e dois patches no módulo doKeybindsStore. - 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_DEAFENetc.) 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 doinputEventUnregisterno 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 aCTRL+SHIFT+F12; o estado passa amatchesTrigger: 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
findocorre uma vez, cadamatchcasa uma vez e o bundle corrigido continua válido. →node check-patches.mjscontradiscord.com/appbaixado. - CA6:
pnpm build --dev,tsc --noEmiteeslintdo plugin sem erros. → comandos. - CA7: Toda função de
logic.tscom 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), logWayland GlobalShortcuts initialized; appId='discord'em toda inicialização (journal); filtra eventos porkeydown/keyup(medido: atalho só-soltar não entrega o pressionar).- Renderer
web.f86c748842ee176f.js, módulo doKeybindsStore: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, componentelB):bound && matchesTrigger===falsemostra aviso comtriggerDescription; regravar a mesma combinação chamareofferWaylandPortalShortcut(reabre o diálogo do GNOME). - mutter
src/core/keybindings.cprocess_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 comCTRL+SHIFT+F12(degetWaylandPortalTrigger) e marcamatchesTrigger:falsesempre. - 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): DispatchModeadjustOptions<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 emFIRST_PROBE_MS(1000); sondar =rearm()(re-registro, zera o estado preso do nativo) + janelaPROBE_WINDOW_MS(300); repetição em atéECHO_MS(150) após o re-registro = ainda segurado; sem repetição = solto; soltura dentro deECHO_MSé eco sintético do re-registro e é ignorada.normalizeTrigger(trigger: string): string→ forma canônicaMODS|teclafixPortalState<S extends PortalState>(state: S, expected: string | undefined): S
index.ts: três patches nofind: "Skipping registration.":$self.wrapRegistration(bridge, id, shortcut, callback, options)(devolve[callback, options]; orearmusa 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 emtry/catchcom fallback para o comportamento original.patches.ts: dados dos patches, usados peloindex.tse pelocheck-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 - feito
T2 · implementar logic.ts · N0
- Serve: CA1–CA4 · Depende de: T1
- Pronto quando: todos os testes passam
- Verificar:
pnpm wk:test→ 0 falhas - feito
T3 · medir CC e conferir caminhos · N0
- Serve: CA7 · Depende de: T2
- Verificar:
uvx lizard logic.ts→ CC ≤ 10 em todas - feito
CP1 · após T1–T3 · N0
- 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 - 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 - feito
T6 · tsc + eslint · N0
- Serve: CA6 · Depende de: T4
- feito
CP2 · após T4–T6 · N0
- feito
T7 · teste real: nativo do Discord + GNOME + logic.ts · N0
- Serve: CA1, S1 · Depende de: T2 · precisa de um toque do usuário
- feito
T8 · teste ao vivo no Discord · N0
- Serve: CA8 · Depende de: CP2 · precisa do ok do usuário para reiniciar o Discord
- 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 nopackage.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
Activatedenquanto acha a tecla pressionada; soltura perdida o deixa preso e o gesto seguinte some. Medido combusctl 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/userpluginsparasrc/plugins(branchfeat/wayland-keybinds, sem commit) comenabledByDefault: true: só altera o comportamento quando o Discord registra pelo portal.