Building a Custom Discord Rich Presence Manager with Ink TUI

Last week I wanted full control over my Discord profile’s rich presence. The default only shows what Discord detects as your “current game” — I wanted custom state, details, buttons, timestamps, and automatic profile rotation.

So I built discord-rpc-tui: a custom Discord Rich Presence manager using Ink (React for the terminal) that runs as a systemd service.


Architecture

Ink TUI (React terminal)     ← or headless mode via systemd

  RPCManager                  ← @xhayper/discord-rpc with auto-reconnect

  Discord IPC socket          ← /run/user/1000/discord-ipc-0

The app has two modes:

  • TUI mode — interactive Ink terminal UI with status bar, profile list, and event log. Use it when you want to see what’s happening.
  • Headless mode — runs as a daemon via systemd, logs to journalctl. Starts on login automatically.
[5:19:58 PM] ○ Connecting...
[5:19:58 PM] Connected to Discord
[5:19:58 PM] Activity set: Coding

 Profile: Coding
 State: Building something cool
 Details: TypeScript | Ink TUI

 [●] q:quit Space:pause n:next r:reload

Key Features

Activity Rotation

Set multiple profiles that rotate automatically at a configurable interval. Each profile has its own state, details, activity type, buttons, and images:

{
  "profiles": [
    {
      "name": "Coding",
      "activity": {
        "state": "Building something cool",
        "details": "TypeScript | Ink TUI",
        "startTimestamp": true,
        "type": 0
      }
    },
    {
      "name": "Gaming",
      "activity": {
        "state": "Exploring Hyrule",
        "details": "Zelda: Tears of the Kingdom",
        "largeImageKey": "zelda",
        "buttons": [{ "label": "Watch Stream", "url": "https://twitch.tv/..." }]
      }
    }
  ],
  "rotationInterval": 600
}

Auto-Reconnect with Exponential Backoff

If Discord restarts or the IPC socket disconnects, RPCManager automatically retries with backoff: 1s → 2s → 4s → 8s → 16s → max 30s

Discord Detection

A DiscordDetector polls the IPC socket at /run/user/1000/discord-ipc-0 every 5 seconds. When Discord closes, it pauses RPC. When Discord opens, it auto-connects.

Activity Title

Each profile now supports a name field that appears as the Discord activity title — e.g., “Playing easy-rag” or “Listening to My Playlist”. This gives full control over how your presence is labeled:

{
  "name": "Coding",
  "activity": {
    "name": "easy-rag",
    "state": "Building something cool",
    "details": "TypeScript • Ink TUI"
  }
}

The Bug That Taught Me Something

The trickiest issue was discovering that @xhayper/discord-rpc’s connect() method doesn’t emit the ready event. You need to call login() instead — which internally calls connect() and then emits ready when no OAuth scopes are needed:

❌ this.client.on('ready', handler);
   await this.client.connect();  // ready NEVER fires

✅ this.client.on('ready', handler);
   await this.client.login();    // ready fires!

The login() method checks if OAuth scopes are required. For simple SET_ACTIVITY, no authentication is needed — just a Client ID. But the name login() is deceptive; it’s actually the correct way to establish a full RPC session even without auth.


Tech Stack

ComponentChoiceWhy
TUI FrameworkInk 7React for terminal, Flexbox layout, first-class keyboard input
RPC Library@xhayper/discord-rpcActive maintenance, TypeScript, IPC + WebSocket transport
ConfigZod + JSONShared dependency with AI SDK, excellent type inference
RuntimeNode.js 22ESM native, systemd integration
TestsVitest — 37 tests across 4 core modules3 new test files (activity-rotator, discord-detector, rpc-manager)
Auto-startsystemd —userStandard Linux user service, restart on failure

Try It

git clone https://github.com/fxckcode/discord-rpc-tui.git
cd discord-rpc-tui
pnpm install
bash install.sh
# Edit ~/.config/discord-rpc-tui/config.json with your Discord Client ID

The install script sets up everything: builds the binary, creates the config, installs the systemd service, and optionally enables auto-start.

The project is MIT licensed and open for contributions on GitHub.

Share
D
Diego Duran
@fxckcode

Backend engineer who ships agentic CLI tooling. NestJS, Go, Django, AWS.

Construyendo un Gestor de Discord Rich Presence con Ink TUI

La semana pasada quería tener control total sobre el rich presence de mi perfil de Discord. El default solo muestra lo que Discord detecta como tu “juego actual” — yo quería estado, detalles, botones, timestamps y rotación automática de perfiles.

Así que construí discord-rpc-tui: un gestor de Discord Rich Presence usando Ink (React para terminal) que corre como servicio systemd.


Arquitectura

Ink TUI (React terminal)     ← o modo headless via systemd

  RPCManager                  ← @xhayper/discord-rpc con reconexión automática

  Socket IPC de Discord       ← /run/user/1000/discord-ipc-0

La app tiene dos modos:

  • Modo TUI — interfaz Ink interactiva con barra de estado, lista de perfiles y log de eventos. Para ver qué está pasando.
  • Modo headless — corre como daemon via systemd, logs a journalctl. Arranca automáticamente al iniciar sesión.
[5:19:58 PM] ○ Connecting...
[5:19:58 PM] Connected to Discord
[5:19:58 PM] Activity set: Coding

 Profile: Coding
 State: Building something cool
 Details: TypeScript | Ink TUI

 [●] q:quit Space:pause n:next r:reload

Características Clave

Rotación de Actividades

Múltiples perfiles que rotan automáticamente. Cada perfil tiene su propio estado, detalles, tipo de actividad, botones e imágenes:

{
  "profiles": [
    {
      "name": "Coding",
      "activity": {
        "state": "Building something cool",
        "details": "TypeScript | Ink TUI",
        "startTimestamp": true,
        "type": 0
      }
    },
    {
      "name": "Gaming",
      "activity": {
        "state": "Exploring Hyrule",
        "details": "Zelda: Tears of the Kingdom",
        "largeImageKey": "zelda",
        "buttons": [{ "label": "Ver Stream", "url": "https://twitch.tv/..." }]
      }
    }
  ],
  "rotationInterval": 600
}

Reconexión Automática con Backoff Exponencial

Si Discord se reinicia o el socket IPC se desconecta, RPCManager reintenta automáticamente: 1s → 2s → 4s → 8s → 16s → máx 30s

Detección de Discord

Un DiscordDetector monitorea el socket IPC en /run/user/1000/discord-ipc-0 cada 5 segundos. Cuando Discord se cierra, pausa el RPC. Cuando Discord se abre, reconecta automáticamente.

Título de Actividad

Cada perfil ahora soporta un campo name que aparece como título de la actividad en Discord — ej., “Playing easy-rag” o “Listening to My Playlist”. Control total sobre cómo se etiqueta tu presencia:

{
  "name": "Coding",
  "activity": {
    "name": "easy-rag",
    "state": "Building something cool",
    "details": "TypeScript • Ink TUI"
  }
}

El Bug Que Me Enseñó Algo

El bug más escurridizo fue descubrir que el método connect() de @xhayper/discord-rpc no emite el evento ready. Hay que llamar a login() — que internamente llama a connect() y luego emite ready cuando no se necesitan scopes OAuth:

❌ this.client.on('ready', handler);
   await this.client.connect();  // ready NUNCA se dispara

✅ this.client.on('ready', handler);
   await this.client.login();    // ready se dispara!

El método login() verifica si se requieren scopes OAuth. Para SET_ACTIVITY, no se necesita autenticación — solo un Client ID. Aunque el nombre login() suene a autenticación, es la forma correcta de establecer una sesión RPC completa incluso sin auth.


Stack Tecnológico

ComponenteElecciónPor qué
TUI FrameworkInk 7React para terminal, Flexbox, input de teclado first-class
RPC Library@xhayper/discord-rpcMantenimiento activo, TypeScript, transporte IPC + WebSocket
ConfigZod + JSONDependencia compartida con AI SDK, inferencia de tipos
RuntimeNode.js 22ESM nativo, integración systemd
TestsVitest — 37 tests en 4 módulos core3 nuevos archivos (activity-rotator, discord-detector, rpc-manager)
Auto-startsystemd —userServicio de usuario Linux estándar, restart on failure

Probarlo

git clone https://github.com/fxckcode/discord-rpc-tui.git
cd discord-rpc-tui
pnpm install
bash install.sh
# Editar ~/.config/discord-rpc-tui/config.json con tu Client ID de Discord

El script de instalación hace todo: compila el binario, crea la configuración, instala el servicio systemd, y opcionalmente habilita el auto-start.

El proyecto tiene licencia MIT y está abierto a contribuciones en GitHub.

Compartir
D
Diego Duran
@fxckcode

Ingeniero backend que construye herramientas CLI agentivas. NestJS, Go, Django, AWS.