Command Palette

Search for a command to run...

All postsEngineering

I Gave My Terminal a Welcome Banner

SB
Sachin Babu
Jul 17, 20267 min read

I wanted my terminal to feel like something. Not just a blinking cursor on a black rectangle — something that actually greets me when I open a new tab. So over a weekend I built a small zsh setup that gives every fresh terminal on my Mac a phosphor-green retro glow, a stripped-down system summary, and a big block-letter "WELCOME SACHIN" banner with the time, date and live system stats.

Nothing about this is technically hard. But it took me longer than I expected because two things went wrong along the way — one because a tool I'd used for years quietly got deleted from Homebrew, and one because a locale variable was missing in a place I never think to look. Here's how I built it, what broke, and how you can steal the setup for yourself.

The finished terminal welcome banner, phosphor green on black
The finished terminal welcome banner, phosphor green on black

The Look I Wanted

Phosphor CRT. Black background, bright green text, a green cursor that blinks. If you've ever seen cool-retro-term you know the vibe. I didn't want the full scanline treatment though — just the colour palette, running inside my normal terminal so it plays nicely with everything else.

Turns out you can set terminal-wide colours from your shell config using OSC escape sequences. These are a different beast from the more familiar ANSI SGR codes that colour individual pieces of text — OSC sequences change properties of the terminal itself, for the whole session.

printf '\033]11;#000000\007'   # background → black
printf '\033]10;#7ee787\007'   # foreground → phosphor green
printf '\033]12;#7ee787\007'   # cursor colour → phosphor green
printf '\033[1 q'              # cursor style → blinking block

Drop those four lines in your .zshrc and every new tab opens straight into the retro theme. Small caveat — macOS's built-in Terminal.app ignores OSC cursor codes, so those two lines only work in iTerm2, WezTerm and similar. Terminal.app users need to set the cursor colour in Settings → Profiles → Text.

neofetch Is Dead — Long Live fastfetch

For the "system logo + specs" line I reached for neofetch, the tool I'd used for years:

brew install neofetch

Homebrew looked happy at first. But which neofetch returned nothing, and brew list neofetch told me why:

Error: No available formula with the name "neofetch"

Turns out neofetch's original maintainer archived the project a while back, and it's since been removed from Homebrew's core repository. If you're following an old dotfiles tutorial that says brew install neofetch, this is why it silently isn't installing.

The actively maintained drop-in is fastfetch:

brew install fastfetch

Same "logo + system info" concept, written in C, faster, still maintained. It's a good general lesson too — pinned tutorials rot, and brew list <formula> is a much better sanity check than trusting the exit code of brew install alone.

Trimming It Down

Out of the box, fastfetch is very keen. On a new tab it wants to print an OS logo plus a wall of specs — OS, host, kernel, uptime, packages, shell, display, WM, theme, font, cursor, terminal, CPU, GPU, memory, swap, disk, local IP, battery, locale and a colour swatch bar. That's a lot for something that runs every time you open a new pane.

Two flags fix it:

fastfetch --logo none --structure "os:host:uptime:memory:disk"

--logo none drops the ASCII art entirely, and --structure takes a colon-separated list of exactly which modules you want. Five fields, no logo, all the useful stuff. Run fastfetch --list-modules if you want to see everything else that's on offer.

The Great UTF-8 Mystery

At this point I added the welcome banner — a rounded box around a status message, then big block-letter text. It looked beautiful in macOS Terminal. Then I opened Cursor's integrated terminal and got this:

�─�─�─�  Session started · all systems nominal  �─�─�

Every box-drawing character (╭ ─ │ ╰) rendered as broken byte soup. Plain ASCII block characters (█) rendered fine. The difference: box-drawing chars are multi-byte UTF-8 codepoints; blocks sit inside single-byte ranges the terminal was happy to interpret.

The diagnostic that gave it away is one command:

locale

In the broken terminal it printed LANG= and LC_ALL= empty, with everything falling back to C / POSIX. Cursor's integrated terminal wasn't inheriting the locale that macOS Terminal sets for you automatically. Fix: pin it explicitly at the very top of .zshrc, before anything styled prints:

export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8

Two lines. If you've ever seen mystery-question-mark characters in a terminal where the same output renders fine elsewhere, this is almost always the reason. And it's a good argument for testing dotfile changes in more than one terminal app before calling them done.

The Banner (and How to Fake a Glow)

The banner has three parts: a rounded status box, the block-letter greeting, and a live time-of-day line. The block letters are just carefully placed █ characters — I sketched them on graph paper first, which felt very 1994 of me.

The "glow" is where ANSI has to cheat. Terminals can't blur pixels, so a true CRT bloom isn't possible without a specialist emulator like cool-retro-term. What you can do is fake the perceptual effect with two shades of green side by side — a bright one for the letters (256-colour code 120) and a very dim one (code 22) for the box outline. The contrast reads as glow, even though nothing is actually glowing.

Same trick lets me do a time-aware greeting with zero dependencies:

hour=$(date +%H)
if   (( hour >= 5  && hour < 12 )); then greeting="Good morning"
elif (( hour >= 12 && hour < 17 )); then greeting="Good afternoon"
elif (( hour >= 17 && hour < 22 )); then greeting="Good evening"
else greeting="Burning the midnight oil"
fi

Stats Without the Tool

I originally thought I'd parse RAM and disk out of fastfetch's output. Then I remembered why I stopped doing that — regex-scraping another tool's output is a bad habit. Every number I wanted was one shell command away:

ram_used=$(ps -A -o rss= | awk '{sum+=$1} END {printf "%.1f", sum/1024/1024}')
ram_total=$(sysctl -n hw.memsize | awk '{printf "%.1f", $1/1024/1024/1024}')
uptime_str=$(uptime | awk -F'up ' '{print $2}' | awk -F',' '{print $1", "$2}')
disk_str=$(df -H / | tail -1 | awk '{print $3" / "$2" used"}')

Simpler, exactly the fields I want, formatted the way I want them. And fast — the whole block runs in a couple of milliseconds. That matters because everything in .zshrc runs on every new tab, and a slow banner is worse than no banner.

The Full Script

Here it is end to end, if you want to lift it wholesale:

# ── Locale fix (prevents garbled box-drawing chars) ──
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8

# ... existing dev-env exports (dotnet, volta, nvm, bun, pnpm) ...

# terminal colours: black background, phosphor green foreground
printf '\033]11;#000000\007'
printf '\033]10;#7ee787\007'
printf '\033]12;#7ee787\007'
printf '\033[1 q'

# fastfetch: minimal, no logo
if command -v fastfetch &> /dev/null; then
  fastfetch --logo none --structure "os:host:uptime:memory:disk"
fi

# welcome banner
hour=$(date +%H)
if   (( hour >= 5  && hour < 12 )); then greeting="Good morning"
elif (( hour >= 12 && hour < 17 )); then greeting="Good afternoon"
elif (( hour >= 17 && hour < 22 )); then greeting="Good evening"
else greeting="Burning the midnight oil"
fi

G=$'\033[1;38;5;120m'   # bright phosphor green
D=$'\033[38;5;22m'      # very dim green
W=$'\033[1;38;5;156m'   # glowing mint
R=$'\033[0m'
B=$'\033[1m'

print ""
print "${D}╭─────────────────────────────────────────╮${R}"
print "${D}│${R} ✻ Session started · all systems ${B}${G}nominal${R} ${D}│${R}"
print "${D}╰─────────────────────────────────────────╯${R}"
print ""
print "${G}█   █ █████ █      ████  ███  █   █ █████${R}"
print "${G}█   █ █     █     █     █   █ ██ ██ █    ${R}"
print "${G}█ █ █ ███   █     █     █   █ █ █ █ ███  ${R}"
print "${G}██ ██ █     █     █     █   █ █   █ █    ${R}"
print "${G}█   █ █████ █████  ████  ███  █   █ █████${R}"
print ""
print "${G} ████  ███   ████ █   █ █████ █   █${R}"
print "${G}█     █   █ █     █   █   █   ██  █${R}"
print "${G} ███  █████ █     █████   █   █ █ █${R}"
print "${G}    █ █   █ █     █   █   █   █  ██${R}"
print "${G}████  █   █  ████ █   █ █████ █   █${R}"
print ""
print "${W}${greeting}, it's $(date '+%H:%M').${R}"
print "${D}$(date '+%A, %d %B')${R}"
print ""

# live stats — one per line
ram_used=$(ps -A -o rss= | awk '{sum+=$1} END {printf "%.1f", sum/1024/1024}')
ram_total=$(sysctl -n hw.memsize | awk '{printf "%.1f", $1/1024/1024/1024}')
uptime_str=$(uptime | awk -F'up ' '{print $2}' | awk -F',' '{print $1", "$2}')
disk_str=$(df -H / | tail -1 | awk '{print $3" / "$2" used"}')

print "${D}RAM     ${W}${ram_used}GB / ${ram_total}GB${R}"
print "${D}Uptime  ${W}${uptime_str}${R}"
print "${D}Disk    ${W}${disk_str}${R}"
print ""
unset hour greeting G D W R B ram_used ram_total uptime_str disk_str

One extra one-off command, run outside .zshrc:

touch ~/.hushlogin

That kills macOS's own "Last login:" line at the very top of every new terminal — otherwise it pushes the banner down and breaks the vibe.

Small Things Worth Knowing

  • macOS Terminal.app ignores OSC cursor codes. If you want the blinking green cursor there, set it in Settings → Profiles → Text. iTerm2, Warp and WezTerm honour the escape codes natively.
  • Test in more than one terminal. The locale bug never showed up in the app I usually use — only in Cursor's embedded one. If your dotfiles are the same everywhere, your bugs won't be.
  • Order matters. The locale export has to come before anything that prints styled output, otherwise the first thing to render sees the wrong encoding. And fastfetch runs before the banner because I like the specs to feel like "system boot" and the banner to feel like the greeting.
  • Keep it light. Everything here executes on every new tab. fastfetch --logo none plus a handful of awk one-liners is close to instant, but if you find yourself reaching for network calls or heavier subprocesses, tuck them behind a flag.
  • Steal It, Twist It

    If you want to riff on this, two directions I'd recommend:

  • Run the block letters through figlet and lolcat (brew install figlet lolcat) for pre-made block-letter fonts and rainbow gradients — a lot less graph paper.
  • If you want real CRT scanlines, glow and bloom, install cool-retro-term (brew install --cask cool-retro-term) and skip the ANSI trickery entirely. It looks incredible, at the cost of no longer being your general-purpose terminal.
  • The whole thing took a Saturday afternoon and about sixty lines of zsh. Every new tab now opens into something that feels like mine, and every time I hit Cmd-T I get a tiny dopamine hit. Worth it.

    All postsEnd of article

    Keep Reading

    All Posts →