Fixing the Invisible Monitor: Curses vs. SSH

Fixing the Invisible Monitor: Curses vs. SSH

My build swarm has a beautiful monitor. It’s written in Python using curses. It has scrolling logs, progress bars, and realtime updates. It looks like the Matrix.

But yesterday, I tried to show it off to a friend remotely.

ssh commander@gateway "build-swarm watch"

Result: Nothing. The terminal cleared, hung for a second, and exited.

No error. No traceback. Just silence.

The Root Cause: Curses Needs a TTY

The Python curses library (and the C library it wraps) is picky. It demands a fully functional TTY (TeleTYpewriter) to initialize.

When you run a command directly over SSH like ssh user@host "command", SSH doesn’t allocate a TTY by default. It just pipes stdout.

Curses tries to call initscr(), realizes “I have no idea how wide this screen is because it’s just a pipe,” and crashes. Or in my case, throws an exception that I was inadvertently swallowing.

The Solution: A “Dumb” Mode

I realized I broke a cardinal rule of CLI design: Always have a fallback.

I updated the build-swarm CLI to perform a simple check before trying to be fancy.

import sys

def is_tty_compatible():
    # 1. Check if stdout is actually a terminal
    if not sys.stdout.isatty():
        return False
        
    # 2. Check for the --tui flag (user forced it)
    if '--tui' in sys.argv:
        return True
        
    # 3. Try a dry-run initialization
    try:
        import curses
        curses.setupterm()
        return True
    except:
        return False

If this check fails, instead of launching the full TUI application, I now launch SimpleMonitor.

The “Simple” Monitor (That’s Actually Better?)

The SimpleMonitor class doesn’t use curses. It just prints text, waits 5 seconds, prints an ANSI clear code (\033[2J), and prints again.

But because I was forced to simplify, I actually made the data clearer.

═══ BUILD SWARM MONITOR ═══
2026-01-14 23:17:42

DRONES & ACTIVE BUILDS:
  drone-Izar [16 cores] - BUILDING: app-shells/bash
  drone-Meridian [18 cores] - IDLE
  drone-Tau-Beta [8 cores] - BUILDING: net-misc/curl
  drone-Tarn [14 cores] - BUILDING: dev-libs/openssl

QUEUE: 10 waiting, 3 building.

It’s ugly. It flashes when it refreshes. But it works everywhere.

  • Inside a CI pipeline? logic works.
  • Piped to a file? Works.
  • Over a shaky SSH connection? Works.

Lesson Learned

Don’t let your vanity (cool UI) get in the way of utility (seeing the data).

I also added a --tui flag so I can force the Matrix mode when I know I’m in a good terminal. But now, my default is safe.

Status: TUI is flashy, stdout is forever.