DEV Community

Karan Raj
Karan Raj

Posted on

I Built a Haunted Terminal That Scares You Into Taking Breaks

Every break reminder I have ever installed died the same death: a toast slid into the corner of the screen, I thought "in a minute", and that was that. The reminder was polite. Polite is ignorable.

So for Hacktoberfest Week 4, whose theme is Ghost in the Machine, I built the opposite: haunt, a terminal ghost that interrupts you with a horror still, a whisper from a local model, and then refuses to give the screen back until you have stood up and looked at something that is not a monitor.

It is deliberately a little mean. That is the health feature.

What it does

Every few minutes (randomised, so you cannot brace for it), haunt:

  1. asks a local open-weight model to write one unsettling line and one concrete break instruction;
  2. slams a horror image, GIF, or video over your terminal;
  3. replaces it with a calm panel that holds a countdown while you breathe, stretch, and walk away;
  4. logs the break so --stats can tell you how long you actually spent away from the screen.
$ python3 haunt.py --demo

your last commit message was a whisper too.

[ a 90x28 frame of a haunted house at night, rendered in 24-bit colour ]

╭──────────────────────────────────────────────────────────────╮
│ the spirit releases you                                      │
│                                                              │
│ Stand up and roll your shoulders back five times.            │
│ The ghost will wait.                                         │
│                                                              │
│ ███████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░       │
│ 14s away from the screen                                     │
╰──────────────────────────────────────────────────────────────╯
  breathe in 4, hold 4, out 6. ctrl+c ends the haunting.
Enter fullscreen mode Exit fullscreen mode

When you quit, it prints the damage: 23 breaks, 46.0 minutes away from the screen.

How it works

flowchart LR
  T[randomised timer] --> O[local model<br/>llama3.2:3b via Ollama]
  O --> W[whisper + health tip]
  T --> A[Wikimedia Commons asset<br/>still, GIF or video]
  A --> C[local cache + attribution]
  A -. network down .-> P[procedural Pillow art]
  W --> R[ANSI half-block renderer]
  C --> R
  P --> R
  R --> B[calm break panel<br/>countdown + breathing]
  B --> L[break log]

No graphics protocol, no browser, no image library in the terminal. Frames are downsampled with Pillow and printed as half-block cells, two vertical pixels per character, with 24-bit colour escapes:

def ansi_frame(img, cols, rows):
    fitted = ImageOps.fit(img.convert("RGB"), (cols, rows * 2),
                          method=Image.Resampling.LANCZOS)
    data = fitted.tobytes()
    lines = []
    for y in range(rows):
        top = (y * 2) * cols * 3
        bottom = (y * 2 + 1) * cols * 3
        chunks = []
        for x in range(cols):
            i, j = top + x * 3, bottom + x * 3
            chunks.append(
                f"\x1b[38;2;{data[i]};{data[i+1]};{data[i+2]}m"
                f"\x1b[48;2;{data[j]};{data[j+1]};{data[j+2]}m▀"
            )
        lines.append("".join(chunks))
    return "\n".join(lines) + "\x1b[0m"
Enter fullscreen mode Exit fullscreen mode

The ghost itself is a prompt. The model returns JSON with three fields, and the app treats every field defensively, because a small model on a laptop will eventually return something strange:

prompt = (
    "You are the ghost in the machine, haunting a developer's terminal to push them "
    "away from the screen for their health. Reply with JSON keys whisper, health_tip, "
    "theme. whisper: one eerie line under 110 characters, unsettling not gory, lowercase, "
    "second person. health_tip: one concrete ergonomic break instruction under 110 "
    "characters that makes the human leave the screen (eyes, posture, movement, water). "
    "theme: exactly one of ghost, haunted-house, skeleton, cemetery, pumpkin, ofrenda."
)
data = ollama_json(prompt, model)
if len(data.get("health_tip", "")) < 20:
    data["health_tip"] = random.choice(FALLBACK_TIPS)
Enter fullscreen mode Exit fullscreen mode

If Ollama is missing, slow, or returns garbage, curated lines take over. The haunting never fails open into silence; it fails into a break, which is the point.

The break panel is a loop with a progress bar and a countdown, not a notification:

start = time.time()
while True:
    remaining = max(0.0, seconds - (time.time() - start))
    if remaining <= 0:
        break
    bar = "█" * int(bar_width * (1 - remaining / seconds))
    draw_panel(tip, bar, remaining)
    time.sleep(0.12)
Enter fullscreen mode Exit fullscreen mode

The only way past it is time, or Ctrl+C, and Ctrl+C still restores the terminal and prints your stats.

Where the horror comes from

Assets are searched and downloaded from Wikimedia Commons using the public MediaWiki API, filtered to images, animated GIFs, and WebM video, and cached locally in ~/.cache/haunt/assets. Every file keeps its artist and licence, which python3 haunt.py --credits ghost prints:

ghost
  Apparition by Eugeniu Voinescu.jpg — Ruth Tillman (CC BY-SA 4.0)
  Athenodorus - The Greek Stoic Philosopher ... — Henry Justice Ford (Public domain)
  La bimba dal vestitino rosso.webm — Museo Italiano Dell'Immaginario Folklorico (CC BY-SA 4.0)
Enter fullscreen mode Exit fullscreen mode

Commons rate-limits aggressively, so downloads retry with backoff and refuse anything over 12 MB. If the network is gone, the ghost paints its own scene with Pillow: a gradient night, fog, a silhouette house, glowing windows, a moon, scratch marks. --offline never touches the network.

Why open matters here

This tool is supposed to change a habit, and habits fail when the tool fails. Open pieces are what make it keep working:

  • A local model means no key, no cost, and no data leaving the machine. The whisper is generated by llama3.2:3b through Ollama. I can run it on a plane, and nothing about my terminal or my work goes to a provider.
  • Swappable models. The prompt is the contract, not an SDK. --model takes anything Ollama can serve, from a 1B model on a laptop to something larger.
  • Offline is a first-class mode. If Commons is unreachable, procedural art and curated lines still produce a complete haunting. The health habit does not depend on a service that might disappear.
  • The media is genuinely reusable. Public domain and Creative Commons assets, with attribution preserved and printable, instead of scraped images with unclear provenance.
  • You can read all of it. The renderer, the model call, the break loop: nothing is hidden behind a hosted API, so you can change how the ghost behaves or how long it holds you.

A note on safety

A scare tool has to be careful about being a scare tool. --safe disables the audio and the single inverted-frame flash, and audio is off by default whenever you ask for safe mode. The flash is one slow pulse, not a strobe. The health advice is ordinary workplace ergonomics: 20-20-20, standing, movement, water, blinking, posture, wrists. It is not medical advice, and the ghost is not a doctor. It is just rude enough to make you stand up.

Run it

git clone https://github.com/karan-2004/haunted-terminal
cd haunted-terminal
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
ollama pull llama3.2:3b

python3 haunt.py --demo     # one haunting now, short break
python3 haunt.py            # haunt roughly every 5 to 9 minutes
python3 haunt.py --safe     # no sound, no flash
python3 haunt.py --stats    # how long you stayed away
Enter fullscreen mode Exit fullscreen mode

It needs Python 3.10+, Pillow, requests, numpy, and Ollama. paplay/aplay are optional for the sound; ffmpeg is optional for Commons videos. No login, no API keys, nothing to configure.

MIT licensed. Built during the Hacktoberfest Open-Source AI Challenge Week 4 window, with the model running locally and the lights off.

Here is the session where it was built, if you want to see the messy middle:

Building haunt: a haunted terminal that forces screen breaks
You

lets create a script that uses a opensource lightweight model to fetch the horror screens, gifs and videos and render it randomly in the terminal to scare the user randomly and make them move away from the screen. let the horror scenes educate users to take breaks and be considerate about the health

Agent

Plan: a local open-weight model (Ollama llama3.2:3b) writes the whisper and the health tip, assets come from Wikimedia Commons (public domain / CC), Pillow renders frames as 24-bit colour half-blocks, and every scare ends in a forced calm break with a countdown. Check the machine first.

You

can browserbase screens be rendered via cmd through agent or model

Agent

Yes, over CDP: Page.startScreencast or captureScreenshot frames rendered with a terminal image tool, or fed to a vision model. Live View is web-only. For this project the simpler path is local: Pillow half-block rendering, no browser needed, which also keeps it fully offline.

Agent
You

lets submit this idea

Agent

Checked the HF26 Week 4 challenge, the DEV account link, and GitHub credentials. Wrote the files, published the repo, and staged the submission post with this session embedded.

Top comments (0)