PatchHub

← patchhub/pomodoro

Big green and blue countdown for the focus timer

Published ✓ Safe by @priya 0 users2 votes 2 files changed (+117 −8)

The terminal Pomodoro timer now shows its countdown in large block digits, green during focus sessions and blue during breaks. The label and progress bar share the same color. In small windows or terminals without color support, it falls back to the old one-line countdown.

What it does

- Show the countdown (MM:SS) as large block digits, 5 lines tall, using a chunky 5x5 font for digits 0-9 and a 3-wide colon; digits are separated by one space. Block pixels use the existing progress-bar fill character. - Layout, indented by 2 spaces: the label line ("Focus" or "Break"), then the 5 digit rows, then a progress bar line in the form "[bar] NNN%" (bar width between 10 and 40 columns, based on terminal width). - Colors: bold bright green (ANSI \033[1;92m) for "Focus"; bold bright blue (\033[1;94m) for "Break". The label, digits and bar all use the session color. - Redraw in place once per second by moving the cursor up over the previous drawing and clearing from there down, rather than appending new lines. - Fallback: if the terminal is too small (digit width >= columns, or height of 7 lines >= terminal lines), use the original single-line display (label, time, bar, percent), still colored. - Color/ANSI support is detected: disabled if stdout isn't a TTY or TERM=dumb. Without ANSI support, use the original one-line carriage-return display with no colors and no cursor movement. - The NO_COLOR environment variable turns off colors but keeps the cursor-based big display. - On Windows, enable virtual terminal processing on the console (needs Windows 10+); if that fails, behave as no-ANSI. - Refactor progress fraction and bar building into helpers so both the big and one-line displays share them. - README: describe the big countdown, green focus / blue break colors, and the fallback to a one-line countdown for small or non-color terminals.

Safety check ✓ Safe

Changes only affect how the timer draws in the terminal (colors, cursor movement) plus a README note. The only system-level call is the standard Windows console setting to enable colors, wrapped in error handling. There are no new dependencies, network access, or file access.

Conversation that created this patch
@priya
Make the countdown big and colorful: green during focus, blue during breaks.
AI
The countdown now shows in large block digits, about five lines tall. It's green during focus sessions and blue during breaks, and the label and progress bar use the same color, so you can tell at a glance which mode you're in. I tested a short run in a simulated terminal, and the big numbers updated in place each second as they should. - **Small windows:** if the window is too small for the big digits, the timer switches back to the old one-line countdown, still in color. - **No colors:** if your terminal can't show colors, it keeps working without them. - **Windows:** colors need Windows 10 or newer. I also added a short note about this to the README.
Code changes (for the curious, 2 files) · raw
README.md +5 −1
@@ -2,9 +2,13 @@
A simple focus timer that runs in your terminal. It follows the
"Pomodoro" technique: work for **25 minutes**, take a **5 minute break**,
−repeat. While it runs you'll see a countdown and a progress bar, and your
+repeat. While it runs you'll see a big countdown and a progress bar, and your
terminal beeps when each focus session or break is over.
+The countdown is shown in large digits, colored **green** while you focus and
+**blue** during breaks. If your terminal window is too small (or doesn't
+support colors), it falls back to a simple one-line countdown.
+
It's a single Python file with nothing extra to install.
## What you need
pomodoro.py +112 −7
@@ -9,6 +9,7 @@ Uses only the Python standard library.
"""
import argparse
+import os
import shutil
import sys
import time
@@ -28,6 +29,62 @@ def pick_bar_chars():
FILL_CHAR, EMPTY_CHAR = pick_bar_chars()
+# Colors for the countdown: green while focusing, blue during breaks.
+RESET = "\033[0m"
+COLORS = {
+ "Focus": "\033[1;92m", # bright green
+ "Break": "\033[1;94m", # bright blue
+}
+
+# A chunky 5-line-tall font for the big countdown digits.
+BIG_FONT = {
+ "0": ["#####", "# #", "# #", "# #", "#####"],
+ "1": [" # ", " ## ", " # ", " # ", " ### "],
+ "2": ["#####", " #", "#####", "# ", "#####"],
+ "3": ["#####", " #", " ####", " #", "#####"],
+ "4": ["# #", "# #", "#####", " #", " #"],
+ "5": ["#####", "# ", "#####", " #", "#####"],
+ "6": ["#####", "# ", "#####", "# #", "#####"],
+ "7": ["#####", " #", " # ", " # ", " # "],
+ "8": ["#####", "# #", "#####", "# #", "#####"],
+ "9": ["#####", "# #", "#####", " #", "#####"],
+ ":": [" ", " # ", " ", " # ", " "],
+}
+BIG_HEIGHT = 5
+INDENT = " "
+
+
+def enable_ansi():
+ """Return True if the terminal can show colors and move the cursor."""
+ if not sys.stdout.isatty() or os.environ.get("TERM") == "dumb":
+ return False
+ if os.name == "nt":
+ # Windows 10+ supports colors, but they have to be switched on.
+ try:
+ import ctypes
+
+ kernel32 = ctypes.windll.kernel32
+ handle = kernel32.GetStdHandle(-11) # standard output
+ mode = ctypes.c_uint32()
+ if not kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
+ return False
+ # 0x0004 = ENABLE_VIRTUAL_TERMINAL_PROCESSING
+ if not kernel32.SetConsoleMode(handle, mode.value | 0x0004):
+ return False
+ except Exception:
+ return False
+ return True
+
+
+ANSI = enable_ansi()
+USE_COLOR = ANSI and "NO_COLOR" not in os.environ
+
+
+def colorize(text, label):
+ if not USE_COLOR or label not in COLORS:
+ return text
+ return COLORS[label] + text + RESET
+
def format_time(seconds):
seconds = max(0, int(round(seconds)))
@@ -38,8 +95,17 @@ def format_time(seconds):
return f"{minutes:02d}:{secs:02d}"
+def progress(remaining, total):
+ return 1.0 if total <= 0 else min(1.0, max(0.0, 1 - remaining / total))
+
+
+def make_bar(fraction, width):
+ filled = int(round(fraction * width))
+ return FILL_CHAR * filled + EMPTY_CHAR * (width - filled)
+
+
def render_line(label, remaining, total):
− fraction = 1.0 if total <= 0 else min(1.0, max(0.0, 1 - remaining / total))
+ fraction = progress(remaining, total)
percent = f"{int(fraction * 100):3d}%"
left = f"{label} {format_time(remaining)} "
right = f" {percent}"
@@ -47,23 +113,62 @@ def render_line(label, remaining, total):
columns = shutil.get_terminal_size((80, 20)).columns
# Leave one column free so the line never wraps on narrow terminals.
bar_width = max(10, min(40, columns - len(left) - len(right) - 3))
− filled = int(round(fraction * bar_width))
− bar = FILL_CHAR * filled + EMPTY_CHAR * (bar_width - filled)
+ bar = make_bar(fraction, bar_width)
return f"{left}[{bar}]{right}"
+def big_digits(text):
+ """Turn a time like "24:59" into 5 lines of large block characters."""
+ rows = []
+ for row in range(BIG_HEIGHT):
+ parts = [BIG_FONT[ch][row] for ch in text]
+ rows.append(" ".join(parts).replace("#", FILL_CHAR).rstrip())
+ return rows
+
+
+def render_big(label, remaining, total):
+ """Return the big countdown as a list of lines, or None if it won't fit."""
+ size = shutil.get_terminal_size((80, 20))
+ digits = big_digits(format_time(remaining))
+ width = len(INDENT) + max(len(row) for row in digits)
+ # Needs room for the title, the digits and the bar, without wrapping.
+ if width >= size.columns or BIG_HEIGHT + 2 >= size.lines:
+ return None
+
+ fraction = progress(remaining, total)
+ percent = f" {int(fraction * 100):3d}%"
+ bar_width = max(10, min(40, size.columns - len(INDENT) - len(percent) - 3))
+ bar = f"[{make_bar(fraction, bar_width)}]{percent}"
+
+ lines = [colorize(INDENT + label, label)]
+ lines += [colorize(INDENT + row, label) for row in digits]
+ lines.append(colorize(INDENT + bar, label))
+ return lines
+
+
def run_session(label, minutes):
"""Count down for the given number of minutes, redrawing once a second."""
total = minutes * 60
end = time.monotonic() + total
last_len = 0
+ prev_lines = 0
while True:
remaining = end - time.monotonic()
− line = render_line(label, max(0.0, remaining), total)
− # Pad with spaces to wipe out any leftover characters from before.
− sys.stdout.write("\r" + line + " " * max(0, last_len - len(line)))
+ if ANSI:
+ lines = render_big(label, max(0.0, remaining), total)
+ if lines is None:
+ # Terminal too small for big digits: fall back to one line.
+ lines = [colorize(render_line(label, max(0.0, remaining), total), label)]
+ # Jump back to the top of the previous drawing and clear it.
+ move_up = f"\033[{prev_lines - 1}A" if prev_lines > 1 else ""
+ sys.stdout.write("\r" + move_up + "\033[J" + "\n".join(lines))
+ prev_lines = len(lines)
+ else:
+ line = render_line(label, max(0.0, remaining), total)
+ # Pad with spaces to wipe out any leftover characters from before.
+ sys.stdout.write("\r" + line + " " * max(0, last_len - len(line)))
+ last_len = len(line)
sys.stdout.flush()
− last_len = len(line)
if remaining <= 0:
break
# Sleep until the next whole second ticks over (keeps it from drifting).