1

RigDeck: uma alternativa ao Stream Deck sem hardware

RigDeck é uma alternativa ao Stream Deck físico. Uma página web abre no
celular, mostra uma grade de botões com pastas, e controla o PC Windows pela
rede local: abre programas, jogos e sites, posiciona janela por monitor,
entra e sai de tela cheia, encerra o que abriu com um toque. Sem app nativo
no celular, sem hardware pra comprar.

Repo: https://github.com/obrenoalvim/rigdeck

Por que

Toda vez que eu ia jogar com amigos, o ritual era o mesmo. Abrir o Discord,
arrastar pra um monitor. Abrir o jogo, arrastar pro outro. Ajustar tamanho de
janela na mão, de novo, porque o Windows nunca lembra onde eu quero cada
coisa. Cinco minutos perdidos antes de cada sessão, todo santo dia.

Um Stream Deck físico resolveria isso, mas custa caro pra um problema
pequeno: eu queria apertar um botão e o PC fazer a coisa certa. Meu celular
já faz isso pra dezenas de outros aparelhos em casa. Faltava fazer pro
próprio PC. Passei um fim de semana nisso.

O que ele faz

Cada botão roda um preset com um ou mais passos. Um passo pode:

  • Abrir um programa, atalho, protocolo (steam://, epicgames://) ou URL,
    posicionando a janela num monitor específico e entrando em tela cheia se
    quiser.
  • Rodar um comando de shell qualquer.
  • Mandar uma tecla (F11, ESC, Alt+Enter) pra janela em foco.

Segurar um botão pressionado encerra exatamente o que aquele preset abriu.
Não mata qualquer processo com nome parecido, mata o PID ou a janela exata
que ele mesmo criou.

As partes chatas de resolver

A ideia é simples. Fazer o Windows cooperar não foi.

Rastrear o processo certo pra fechar depois. Um .exe você lança e pega
o PID direto. Mas boa parte do que eu queria controlar não é assim: sites
abertos no navegador, scripts do AutoHotkey sem janela nenhuma, ou o launcher
de um jogo que sobe um processo, fecha, e entrega pra outro processo com nome
diferente. Cada caso pediu uma estratégia própria de rastreamento.

Fechar só uma aba do navegador, não o navegador inteiro. O Chrome e
derivados rodam um processo só compartilhado entre todas as janelas e abas do
mesmo perfil. Matar por PID mata tudo. A saída foi abrir a URL em modo
--app= (janela própria, sem abas) e, como nem Process.MainWindowHandle do
.NET ajuda aqui (só devolve uma janela por processo, e o Windows pode trocar
qual é "a principal" a qualquer momento), listar todas as HWND do processo
antes e depois de abrir, e tratar a que apareceu como a janela certa. Depois
fecha só ela, com WM_CLOSE, igual clicar no X.

F11 nem sempre é F11. Testando com um jogo mobile rodando via emulador do
Google Play Games (uma janela de VM, sem tratamento próprio de tela cheia),
mandar a tecla F11 não fazia nada. O emulador não escuta essa tecla. A
solução foi trocar por manipulação de janela direto: tirar o estilo
WS_CAPTION/WS_THICKFRAME e esticar pro tamanho exato da tela via
SetWindowPos. O mesmo truque que jogo sem fullscreen nativo usa por baixo
dos panos.

Script sem janela trava o posicionamento. O código original só
considerava um processo "pronto" quando ele tinha uma MainWindowHandle
visível. Scripts do AutoHotkey rodam em segundo plano, sem janela nenhuma, e
o botão ficava até 60 segundos esperando uma janela que nunca ia aparecer,
antes de desistir sem nem capturar o PID. Corrigido: depois de um tempo curto
sem janela, se o processo existe, ele já devolve o PID mesmo assim, e só pula
o posicionamento visual.

Arquitetura

celular → API Fastify (Node + TypeScript) → scripts PowerShell → Win32 API

O frontend é JavaScript puro, sem framework, sem build step. O backend
recebe "roda o preset X", resolve os passos e chama o executor, que decide
como cada passo roda. A camada que conversa com o Windows fica isolada
em scripts PowerShell: enumera monitores, move janela via SetWindowPos,
extrai ícone de .exe, descobre jogos instalados no Steam e na Epic lendo os
manifestos deles.

A versão do repo público troca o Express original por Fastify, só pra deixar
a base mais próxima do que eu usaria em produção de verdade. Nessa migração
encontrei um efeito colateral chato: o parser padrão de JSON do Fastify
rejeita corpo vazio quando o Content-Type diz application/json, e o
frontend manda esse header em toda chamada, inclusive nas que não têm corpo
(DELETE, /run, /kill). Sem ajustar isso, boa parte dos botões
quebraria em produção. Achei o bug rodando os testes de verdade contra o
servidor, não só lendo o código.

Limitações, sem enfeitar

Só roda em Windows, porque todo o controle de janela depende de Win32 API.
Pensado pra uma máquina, um usuário, uma rede local: não tem autenticação, e
não deve ser exposto direto pra internet. O rastreamento fino de "o que
fechar" vive em memória, então reiniciar o servidor perde essa precisão e o
fechamento cai de volta pra matar por nome de processo.

É a solução que resolveu meu problema específico, publicada como referência
pra quem quiser adaptar pro próprio setup.

Repo: https://github.com/obrenoalvim/rigdeck

Carregando publicação patrocinada...