Fetching latest headlines…
Stop Writing Ugly Terminal Output in Node.js
NORTH AMERICA
πŸ‡ΊπŸ‡Έ United Statesβ€’April 18, 2026

Stop Writing Ugly Terminal Output in Node.js

0 views0 likes0 comments
Originally published byDev.to

A zero-dependency Node.js utility for colored logs, spinners, progress bars, tables, tree views, diffs, and OS notifications β€” all without looking up a single ANSI code.

You know the drill. You're deep into a Node.js CLI tool or a long-running script. The terminal output looks like this:

processing...
done
error
done

Completely useless. No color. No structure. No idea what happened.

So you reach for chalk, then ora, then cli-table3, then diff, and suddenly your package.json has five new dependencies for the sole purpose of making console.log bearable.

budgie-console is a single package that handles all of it β€” with zero external dependencies.

npm install budgie-console
const Console = require('budgie-console');

That's the whole setup. Let's look at what you get.

The basics: colored log levels

Four methods that do what you actually want from a log level:

Console.success('Server started on port 3000');   // βœ”  green
Console.error('Database connection refused');      // βœ–  red
Console.warn('NODE_ENV is not set');               // ⚠  yellow
Console.info('Running Node ' + process.version);   // β„Ή  cyan

And the core log() method if you want full control over colors yourself:

Console.log(Console.Bright + Console.FgCyan, 'Bold cyan');
Console.log(Console.Underscore + Console.BgYellow, 'Underlined on yellow');
Console.log(Console.FgRed, 'Custom red message');

All ANSI color and style constants are exposed directly β€” FgRed, BgGreen, Bright, Dim, Underscore, and the rest β€” so you can compose them freely without memorising escape sequences. Colors automatically disable themselves when NO_COLOR is set or the terminal is dumb.

Spinner β€” with a completion message

The spinner animates in-place using \r and stops cleanly when your statusFn returns false. The new doneMessage parameter means you don't need to awkwardly call Console.success() yourself afterwards and risk a race condition:

let running = true;

Console.spinner(
  ['β ‹', 'β ™', 'β Ή', 'β Έ', 'β Ό', 'β ΄', 'β ¦', 'β §', 'β ‡', '⠏'],
  'Fetching remote config...',
  80,
  () => running,
  'Config loaded!'       // ← printed atomically when spinner stops
);

fetchConfig().then(() => { running = false; });
βœ”  Config loaded!

It also returns a cancel function if you need to stop it imperatively:

const stop = Console.spinner(frames, 'Building...', 100, () => true);
// later:
stop();

Progress bar β€” with cleanup

Call progress() from any loop or interval. When current >= total, it prints a newline so subsequent output starts on a fresh line (not tacked onto the end of the bar line β€” a subtle bug that's been fixed). Optional doneMessage here too:

let i = 0;
const iv = setInterval(() => {
  Console.progress(i, 50, 30, Console.FgCyan, 'Upload complete');
  if (++i > 50) clearInterval(iv);
}, 40);
[β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘] 93%
βœ”  Upload complete

Table β€” handles any cell type

Render bordered tables from a 2D array. Pass an optional header row as the second argument. Cell values don't need to be strings β€” numbers, booleans, null, and undefined are all coerced safely, so you can pass data directly from your objects without .toString()-ing everything:

Console.table(
  [
    ['Alice',   28,   true,  'Engineer'],
    ['Bob',     34,   false, 'Designer'],
    ['Charlie', null, true,  'Intern'  ],
  ],
  ['Name', 'Age', 'Active', 'Role']
);
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Name    β”‚ Age  β”‚ Active β”‚ Role     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Alice   β”‚ 28   β”‚ true   β”‚ Engineer β”‚
β”‚ Bob     β”‚ 34   β”‚ false  β”‚ Designer β”‚
β”‚ Charlie β”‚ null β”‚ true   β”‚ Intern   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Tree β€” pretty-print nested objects

Ever wanted something like util.inspect but actually readable in a terminal? Console.tree() renders any JS object or array as an indented tree with proper branch characters, like the Unix tree command. Primitives are color-coded: strings in green, numbers in cyan, booleans and null in yellow.

Console.tree({
  server: {
    host: 'localhost',
    port: 3000,
    ssl: false,
  },
  db: {
    name: 'myapp_prod',
    pool: 5,
    replicas: ['db1', 'db2'],
  },
}, 'config');
└── config
    β”œβ”€β”€ server
    β”‚   β”œβ”€β”€ host: "localhost"
    β”‚   β”œβ”€β”€ port: 3000
    β”‚   └── ssl: false
    └── db
        β”œβ”€β”€ name: "myapp_prod"
        β”œβ”€β”€ pool: 5
        └── replicas
            β”œβ”€β”€ 0: "db1"
            └── 1: "db2"

Incredibly useful when debugging config objects, parsed JSON responses, or AST nodes. The second argument is the label for the root node β€” defaults to 'root'.

Diff β€” colored line-by-line comparison

No external dependency. Pure string comparison, line by line. Removed lines in red with -, added lines in green with +, unchanged lines dimmed. Perfect for showing what changed in a config file, an API response, or a generated file before writing it to disk:

const before = `
  host: localhost
  port: 3000
  debug: true
`.trim();

const after = `
  host: localhost
  port: 8080
  debug: true
  logLevel: info
`.trim();

Console.diff(before, after);
  host: localhost
- port: 3000
+ port: 8080
  debug: true
+ logLevel: info

No LCS algorithm, no diffing library, no download. Works fine for config summaries, migration previews, and anything where you're comparing two versions of a string.

Notify β€” OS desktop notifications

When a long build finishes, you don't want to keep staring at the terminal. Console.notify() fires a native OS notification so you can switch tabs and come back when it's done:

Console.notify('Build complete', 'Your production bundle is ready.');

It shells out to osascript on macOS, notify-send on Linux, and PowerShell's NotifyIcon on Windows. No npm packages. If the underlying command isn't found, it warns to the terminal instead of throwing, so it degrades gracefully in CI or SSH sessions.

Linux note: requires libnotify β€” sudo apt install libnotify-bin on Debian/Ubuntu.

The utilities you already know

A few more methods that round out the toolkit:

Box β€” wraps text in a bordered box, good for section headers in verbose output:

Console.box('Deployment complete', Console.FgGreen);
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Deployment complete  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Divider β€” horizontal rule to visually separate output sections:

Console.divider();                           // ──────────── (40 chars, dim)
Console.divider('═', 44, Console.FgCyan);

Prompt β€” async readline input, returns a Promise:

const name = await Console.prompt('Your name:');
Console.success(`Hello, ${name}`);

Clear β€” clears the terminal:

Console.clear();

Install and use

npm install budgie-console
const Console = require('budgie-console');

Console.success('Ready');
Console.table([['key', 'value']], ['Param', 'Result']);
Console.tree({ build: { env: 'production', minify: true } });
Console.notify('Done', 'Script finished');

Zero config. Zero dependencies. Requires Node.js >=14. ANSI codes work out of the box on macOS and Linux; on Windows they work in Windows Terminal and VS Code's integrated terminal.

Source is on GitHub β€” yashdatir/budgie-console. Issues, PRs, and feedback welcome.

What do you reach for when you need terminal output to not look terrible? Drop it in the comments β€” always curious what other people are using.

Comments (0)

Sign in to join the discussion

Be the first to comment!