Motivational quote under the focus timer
During each focus session, the timer shows a motivational quote on the line below the countdown. A different quote is picked each round, breaks show none, and a new `--no-quotes` option turns the feature off.
What it does
- During each focus session, display one motivational quote beneath the countdown/progress bar line. Breaks show no quote.
- Include a built-in list of about 20 well-known quotes, each with an author (e.g. "The secret of getting ahead is getting started." - Mark Twain). Pick one at random at the start of each focus round, never repeating the quote from the immediately preceding round.
- Format as: “quote text” — author, with curly quotes and an em dash. If the terminal's output encoding can't represent those characters, fall back to straight quotes (") and a hyphen (-).
- Indent the quote by 2 spaces. Wrap it to the terminal width minus 1 (minimum width 20), with continuation lines indented by 3 spaces. Use 80 columns if the width is unknown.
- Print the quote lines below the countdown line, then move the cursor back up to the countdown line (ANSI cursor-up) so the live countdown keeps redrawing above the quote. When the session ends or is interrupted, move the cursor below the quote.
- Only use cursor movement when stdout is an interactive terminal. On Windows, try to enable virtual terminal processing for the console; if that fails, treat cursor movement as unsupported.
- If cursor movement is unsupported, print the quote on the lines just above the countdown instead.
- Add a `--no-quotes` command-line flag that disables quotes entirely. Document it in the help text and README (options table plus a sentence in the intro about quotes).
- On Ctrl+C, print a single newline before "Stopped." (previously two), since the session cleanup already moves to a fresh line.
Safety check ✓ Safe
The change is local and does only what was asked: it adds a hardcoded list of quotes, display formatting, and an opt-out flag. The only system-level piece is a Windows console call that enables cursor movement. There are no network requests, new dependencies, or file access beyond standard-library modules.
Conversation that created this patch
@sam-k
Show a motivational quote under the timer during each focus session.
AI
I've added motivational quotes to your timer. When a focus session starts, a quote appears on the line under the countdown and progress bar, and it stays there while the timer ticks. Each round shows a different quote, picked at random from about 20 well-known ones, and you never get the same quote twice in a row. Breaks don't show a quote.
A few things to know:
- **Turning it off:** I also added a `--no-quotes` option in case you ever want the plain timer back. It's listed in the help and the README.
- **Fit:** Long quotes wrap neatly to fit narrow terminal windows.
- **Older terminals:** In the rare terminal that can't keep the quote underneath, it shows just above the timer instead.
I tried it in a simulated terminal: the quote appeared under the timer in every round, and stopping with Ctrl+C mid-session still exited cleanly. I couldn't test it on Windows, though.
Code changes (for the curious, 2 files) · raw
README.md +4 −1
@@ -3,7 +3,9 @@ | |
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 | |
| − | terminal beeps when each focus session or break is over. |
| + | terminal beeps when each focus session or break is over. During each focus |
| + | session a motivational quote is shown under the timer (a different one each |
| + | round). |
| |
It's a single Python file with nothing extra to install. | |
| |
@@ -38,6 +40,7 @@ You can change the times if you want: | |
| `--break MINUTES` | Length of each break (default 5) | `--break 10` | | |
| `--rounds N` | Stop after N focus sessions (default: keep going) | `--rounds 4` | | |
| `--quiet` | Don't beep | `--quiet` | | |
| + | | `--no-quotes` | Don't show motivational quotes | `--no-quotes` | |
| |
For example, four rounds of 50 minutes work with 10 minute breaks: | |
|
pomodoro.py +128 −17
@@ -3,14 +3,18 @@ | |
| |
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. |
| + | ringing the terminal bell when each session ends. A motivational quote is |
| + | shown under the timer during each focus session. |
| |
Uses only the Python standard library. | |
""" | |
| |
import argparse | |
| + | import os |
| + | import random |
import shutil | |
import sys | |
| + | import textwrap |
import time | |
| |
BELL = "\a" | |
@@ -29,6 +33,84 @@ def pick_bar_chars(): | |
FILL_CHAR, EMPTY_CHAR = pick_bar_chars() | |
| |
| |
| + | QUOTES = [ |
| + | ("The secret of getting ahead is getting started.", "Mark Twain"), |
| + | ("It always seems impossible until it's done.", "Nelson Mandela"), |
| + | ("Focus on being productive instead of busy.", "Tim Ferriss"), |
| + | ("You don't have to see the whole staircase, just take the first step.", |
| + | "Martin Luther King Jr."), |
| + | ("Well done is better than well said.", "Benjamin Franklin"), |
| + | ("Action is the foundational key to all success.", "Pablo Picasso"), |
| + | ("It does not matter how slowly you go as long as you do not stop.", |
| + | "Confucius"), |
| + | ("The way to get started is to quit talking and begin doing.", |
| + | "Walt Disney"), |
| + | ("Quality is not an act, it is a habit.", "Aristotle"), |
| + | ("Small deeds done are better than great deeds planned.", "Peter Marshall"), |
| + | ("Concentrate all your thoughts upon the work at hand.", |
| + | "Alexander Graham Bell"), |
| + | ("Do what you can, with what you have, where you are.", |
| + | "Theodore Roosevelt"), |
| + | ("Success is the sum of small efforts, repeated day in and day out.", |
| + | "Robert Collier"), |
| + | ("A year from now you may wish you had started today.", "Karen Lamb"), |
| + | ("Done is better than perfect.", "Sheryl Sandberg"), |
| + | ("You will never always be motivated, so you must learn to be " |
| + | "disciplined.", "Unknown"), |
| + | ("Great things are done by a series of small things brought together.", |
| + | "Vincent van Gogh"), |
| + | ("Don't watch the clock; do what it does. Keep going.", "Sam Levenson"), |
| + | ("Energy and persistence conquer all things.", "Benjamin Franklin"), |
| + | ("The future depends on what you do today.", "Mahatma Gandhi"), |
| + | ] |
| + | |
| + | |
| + | def pick_quote(previous=None): |
| + | """Pick a random quote, avoiding the one shown last time.""" |
| + | choices = [q for q in QUOTES if q != previous] or QUOTES |
| + | return random.choice(choices) |
| + | |
| + | |
| + | def format_quote(quote): |
| + | """Turn a (text, author) pair into lines that fit the terminal.""" |
| + | text, author = quote |
| + | encoding = getattr(sys.stdout, "encoding", None) or "ascii" |
| + | try: |
| + | "\u201c\u201d\u2014".encode(encoding) |
| + | open_q, close_q, dash = "\u201c", "\u201d", "\u2014" |
| + | except (UnicodeEncodeError, LookupError): |
| + | open_q, close_q, dash = '"', '"', "-" |
| + | full = f"{open_q}{text}{close_q} {dash} {author}" |
| + | columns = shutil.get_terminal_size((80, 20)).columns |
| + | # Leave one column free so lines never wrap on their own. |
| + | width = max(20, columns - 1) |
| + | return textwrap.wrap(full, width=width, initial_indent=" ", |
| + | subsequent_indent=" ") |
| + | |
| + | |
| + | def enable_cursor_moves(): |
| + | """Return True if the terminal lets us move the cursor up a line. |
| + | |
| + | That's needed to keep the quote sitting under the live countdown. On |
| + | Windows this switches on the console's support for it if possible. |
| + | """ |
| + | if not (hasattr(sys.stdout, "isatty") and sys.stdout.isatty()): |
| + | return False |
| + | if os.name != "nt": |
| + | return True |
| + | 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 |
| + | return bool(kernel32.SetConsoleMode(handle, mode.value | 0x0004)) |
| + | except Exception: |
| + | return False |
| + | |
| + | |
def format_time(seconds): | |
seconds = max(0, int(round(seconds))) | |
minutes, secs = divmod(seconds, 60) | |
@@ -52,24 +134,42 @@ def render_line(label, remaining, total): | |
return f"{left}[{bar}]{right}" | |
| |
| |
| − | def run_session(label, minutes): |
| − | """Count down for the given number of minutes, redrawing once a second.""" |
| + | def run_session(label, minutes, quote_lines=None, can_move_cursor=False): |
| + | """Count down for the given number of minutes, redrawing once a second. |
| + | |
| + | If quote_lines is given, they are shown under the countdown (or above it |
| + | if the terminal can't move the cursor). |
| + | """ |
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))) |
| + | below = 0 # how many quote lines sit under the countdown line |
| + | if quote_lines: |
| + | if can_move_cursor: |
| + | # Print the quote, then hop back up to the countdown line. |
| + | sys.stdout.write("\n" + "\n".join(quote_lines)) |
| + | below = len(quote_lines) |
| + | sys.stdout.write(f"\x1b[{below}A\r") |
| + | else: |
| + | sys.stdout.write("\n".join(quote_lines) + "\n") |
| + | sys.stdout.flush() |
| + | try: |
| + | 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)) |
| + | finally: |
| + | # Move past the countdown (and the quote, if it's underneath). |
| + | sys.stdout.write("\n" * (below + 1)) |
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): | |
@@ -122,6 +222,10 @@ def parse_args(argv=None): | |
"-q", "--quiet", action="store_true", | |
help="don't ring the terminal bell", | |
) | |
| + | parser.add_argument( |
| + | "--no-quotes", action="store_true", |
| + | help="don't show a motivational quote during focus sessions", |
| + | ) |
return parser.parse_args(argv) | |
| |
| |
@@ -137,13 +241,20 @@ def main(argv=None): | |
print(f"Pomodoro timer: {focus_text} focus, {break_text} break.") | |
print("Press Ctrl+C to stop.\n") | |
| |
| + | can_move_cursor = enable_cursor_moves() |
| + | quote = None |
| + | |
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) |
| + | quote_lines = None |
| + | if not args.no_quotes: |
| + | quote = pick_quote(quote) |
| + | quote_lines = format_quote(quote) |
| + | run_session("Focus", args.focus, quote_lines, can_move_cursor) |
completed += 1 | |
ring(ring_times) | |
| |
@@ -158,7 +269,7 @@ def main(argv=None): | |
print("Break over! Back to work.\n") | |
round_number += 1 | |
except KeyboardInterrupt: | |
| − | print("\n\nStopped.") |
| + | print("\nStopped.") |
| |
print(f"Focus sessions completed: {completed}. Nice work!") | |
return 0 |