#!/usr/bin/env python3 """A simple Pomodoro focus timer for the terminal. Alternates between focus sessions (25 minutes by default) and breaks (5 minutes by default), showing a live countdown with a progress bar and ringing the terminal bell when each session ends. Uses only the Python standard library. """ import argparse import shutil import sys import time BELL = "\a" def pick_bar_chars(): """Use nice block characters if the terminal can show them, else ASCII.""" encoding = getattr(sys.stdout, "encoding", None) or "ascii" try: "█░".encode(encoding) return "█", "░" except (UnicodeEncodeError, LookupError): return "#", "-" FILL_CHAR, EMPTY_CHAR = pick_bar_chars() def format_time(seconds): seconds = max(0, int(round(seconds))) minutes, secs = divmod(seconds, 60) hours, minutes = divmod(minutes, 60) if hours: return f"{hours}:{minutes:02d}:{secs:02d}" return f"{minutes:02d}:{secs:02d}" def render_line(label, remaining, total): fraction = 1.0 if total <= 0 else min(1.0, max(0.0, 1 - remaining / total)) percent = f"{int(fraction * 100):3d}%" left = f"{label} {format_time(remaining)} " right = f" {percent}" 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) return f"{left}[{bar}]{right}" 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 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))) sys.stdout.flush() last_len = len(line) if remaining <= 0: break # Sleep until the next whole second ticks over (keeps it from drifting). time.sleep(min(remaining, (remaining % 1) or 1)) sys.stdout.write("\n") sys.stdout.flush() def ring(times): for i in range(times): sys.stdout.write(BELL) sys.stdout.flush() if i < times - 1: time.sleep(0.4) def positive_number(text): try: value = float(text) except ValueError: raise argparse.ArgumentTypeError(f"'{text}' is not a number") if value <= 0: raise argparse.ArgumentTypeError("must be greater than 0") return value def non_negative_int(text): try: value = int(text) except ValueError: raise argparse.ArgumentTypeError(f"'{text}' is not a whole number") if value < 0: raise argparse.ArgumentTypeError("must be 0 or more") return value def parse_args(argv=None): parser = argparse.ArgumentParser( description="A simple Pomodoro focus timer for the terminal. " "Press Ctrl+C at any time to stop." ) parser.add_argument( "-f", "--focus", type=positive_number, default=25, metavar="MINUTES", help="length of each focus session in minutes (default: 25)", ) parser.add_argument( "-b", "--break", dest="break_minutes", type=positive_number, default=5, metavar="MINUTES", help="length of each break in minutes (default: 5)", ) parser.add_argument( "-r", "--rounds", type=non_negative_int, default=0, metavar="N", help="how many focus sessions to do; 0 means keep going until " "you press Ctrl+C (default: 0)", ) parser.add_argument( "-q", "--quiet", action="store_true", help="don't ring the terminal bell", ) return parser.parse_args(argv) def main(argv=None): args = parse_args(argv) ring_times = 0 if args.quiet else 3 def plural(n): return "minute" if n == 1 else "minutes" focus_text = f"{args.focus:g} {plural(args.focus)}" break_text = f"{args.break_minutes:g} {plural(args.break_minutes)}" print(f"Pomodoro timer: {focus_text} focus, {break_text} break.") print("Press Ctrl+C to stop.\n") completed = 0 try: round_number = 1 while args.rounds == 0 or round_number <= args.rounds: suffix = f"/{args.rounds}" if args.rounds else "" print(f"Round {round_number}{suffix}") run_session("Focus", args.focus) completed += 1 ring(ring_times) is_last = args.rounds and round_number >= args.rounds if is_last: print("Focus session done!") break print("Focus session done! Time for a break.") run_session("Break", args.break_minutes) ring(ring_times) print("Break over! Back to work.\n") round_number += 1 except KeyboardInterrupt: print("\n\nStopped.") print(f"Focus sessions completed: {completed}. Nice work!") return 0 if __name__ == "__main__": sys.exit(main())