PatchHub

← patchhub/pomodoro

Motivational quote under the focus timer

Published ✓ Safe by @sam-k 0 users2 votes 2 files changed (+132 −18)

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