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.