Tier 0 — Absolute Beginner

Countdown Timer

Count down from any duration in the terminal — setInterval, clearInterval, and in-place display with \r.

🐍 Python 🟨 JavaScript

1. The Problem

Build a countdown timer that accepts a duration (e.g. node timer.mjs 5m or node timer.mjs 90s), displays a live countdown that updates in place, and plays a bell when it hits zero.

Why this project? setInterval, clearInterval, for in-place terminal output, and parsing human-readable time strings — all in one small program.

2. How to Think About This

Parse the input into total seconds. Each second, decrement and redraw the display using (carriage return without newline — rewrites the same line). When seconds hit zero, clear the interval and ring the terminal bell ().

3. The Build

timer.mjs
export function parseTime(input) {
  if (!input) throw new Error("Provide a duration: 90s, 5m, 1h30m");
  const hours   = +(input.match(/(\d+)h/)?.[1] ?? 0);
  const minutes = +(input.match(/(\d+)m/)?.[1] ?? 0);
  const seconds = +(input.match(/(\d+)s/)?.[1] ?? 0);
  const total   = hours * 3600 + minutes * 60 + seconds;
  if (total <= 0) throw new RangeError("Duration must be greater than zero");
  return total;
}

export function formatTime(seconds) {
  const h = Math.floor(seconds / 3600);
  const m = Math.floor((seconds % 3600) / 60);
  const s = seconds % 60;
  return h
    ? `${String(h).padStart(2,"0")}:${String(m).padStart(2,"0")}:${String(s).padStart(2,"0")}`
    : `${String(m).padStart(2,"0")}:${String(s).padStart(2,"0")}`;
}

const rawInput = process.argv[2];
let remaining;
try {
  remaining = parseTime(rawInput);
} catch (e) {
  console.error(e.message);
  process.exit(1);
}

console.log(`Starting: ${formatTime(remaining)}
`);

const interval = setInterval(() => {
  process.stdout.write(`
  ${formatTime(remaining)}  `);
  if (remaining === 0) {
    clearInterval(interval);
    process.stdout.write("
");
    process.stdout.write(""); // terminal bell
    console.log("  ✓ Time's up!");
    process.exit(0);
  }
  remaining--;
}, 1000);

4. Test & Prove

import { parseTime, formatTime } from "./timer.mjs";
let p=0,f=0;
function test(d,fn){try{fn();console.log("  ✓",d);p++;}catch(e){console.log("  ✗",d,"—",e.message);f++;}}
test("90s = 90",    () => { if(parseTime("90s")!==90) throw new Error(); });
test("5m = 300",    () => { if(parseTime("5m")!==300) throw new Error(); });
test("1h = 3600",   () => { if(parseTime("1h")!==3600) throw new Error(); });
test("1h30m = 5400",() => { if(parseTime("1h30m")!==5400) throw new Error(); });
test("format 90s",  () => { if(formatTime(90)!=="01:30") throw new Error(formatTime(90)); });
test("format 3600", () => { if(formatTime(3600)!=="01:00:00") throw new Error(); });
test("format 0",    () => { if(formatTime(0)!=="00:00") throw new Error(); });
test("zero throws", () => { try{parseTime("0s");throw new Error("no");}catch(e){if(e.message==="no")throw e;} });
console.log(`
${p} passed, ${f} failed`);

5. The Interface

node timer.mjs 90s    # 1 minute 30 seconds
node timer.mjs 5m     # 5 minutes
node timer.mjs 1h30m  # 1 hour 30 minutes
# Updates in place: 01:29  01:28  01:27 ...
# When done: ✓ Time's up! + terminal bell

6. Run It

node timer.mjs 30s
node timer.test.mjs

🎯 Try this next

  1. Add a --label flag: node timer.mjs 25m --label "Focus session"
  2. Add a pause/resume — listen for a keypress using process.stdin.setRawMode
  3. Log completed sessions to a JSON file (mini Pomodoro tracker)
  4. Build a browser version with a circular SVG progress ring

What you practised

Regex-based time string parsing, setInterval/clearInterval, in-place terminal output with , the terminal bell escape (), and separating pure parse/format logic from the timer loop for testability.

Reference: Regular Expressions · Async & Promises

Try It Yourself