From 662fce9ff62ad3c99fdf17c381f97544579e5357 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Fri, 7 Nov 2025 21:11:25 -0800 Subject: [PATCH] Add support for 3 digit minutes Includes changes from #210 for the newly refactored code Co-authored-by: Adrian Sampson --- src/countdown/__main__.py | 2 +- src/countdown/display.py | 33 +++++++++++++++++++++++---------- src/countdown/timer.py | 4 ++-- tests/test_display.py | 15 ++++++++++++++- tests/test_timer.py | 16 ++++++++++++++++ 5 files changed, 56 insertions(+), 14 deletions(-) diff --git a/src/countdown/__main__.py b/src/countdown/__main__.py index db73d12..1501787 100644 --- a/src/countdown/__main__.py +++ b/src/countdown/__main__.py @@ -26,7 +26,7 @@ def get_number_lines(seconds): """Return list of lines which make large MM:SS glyphs for given seconds.""" - return timer.get_number_lines(seconds, get_chars_for_terminal()) + return timer.get_number_lines(seconds, get_chars_for_terminal(seconds)) def run_countdown(total_seconds): diff --git a/src/countdown/display.py b/src/countdown/display.py index bb32a4b..9ebf38d 100644 --- a/src/countdown/display.py +++ b/src/countdown/display.py @@ -35,21 +35,34 @@ def enable_ansi_escape_codes(): # pragma: no cover ) -def get_required_width(chars): - """Calculate the minimum width required to display MM:SS format.""" - # MM:SS format has 4 digits, 1 colon, and 1 space after each char - digit_width = max(len(line) for line in chars["0"].splitlines()) - colon_width = max(len(line) for line in chars[":"].splitlines()) - # Total: 4 digits + 1 colon + 5 spaces (after each character) - return digit_width * 4 + colon_width + 5 +def _format_time_string(seconds): + """Return the MM:SS string used for display based on seconds.""" + seconds = max(0, int(seconds)) + minutes, seconds = divmod(seconds, 60) + return f"{minutes:02d}:{seconds:02d}" -def get_chars_for_terminal(): - """Return the largest CHARS dictionary that fits in the current terminal.""" +def get_required_width(chars, time_string): + """Calculate the minimum width required to display the given time string.""" + char_widths = { + char: max(len(line) for line in glyph.splitlines()) + for char, glyph in chars.items() + } + # Each character in the timer output has a trailing space appended + return sum(char_widths[char] + 1 for char in time_string) + + +def get_chars_for_terminal(seconds=0): + """Return the largest CHARS dictionary that fits in the current terminal. + + Args: + seconds: Current countdown value, used to account for wide minute values. + """ width, height = shutil.get_terminal_size() + time_string = _format_time_string(seconds) for size in DIGIT_SIZES: chars = CHARS_BY_SIZE[size] - required_width = get_required_width(chars) + required_width = get_required_width(chars, time_string) # For size 3 (smallest multi-line), allow it without padding # For larger sizes, require 1 line of padding on top and bottom (2 total) padding_needed = 0 if size == 3 else 2 diff --git a/src/countdown/timer.py b/src/countdown/timer.py index 8a6e288..8dc7bd3 100644 --- a/src/countdown/timer.py +++ b/src/countdown/timer.py @@ -6,11 +6,11 @@ r""" ^ (?: # Optional minutes - ( \d{1,2} ) # D or DD + ( \d+ ) # one or more digits m # "m" )? (?: # Optional seconds - ( \d{1,2} ) # D or DD + ( \d+ ) # one or more digits s # "s" )? $ diff --git a/tests/test_display.py b/tests/test_display.py index 75f9863..7393316 100644 --- a/tests/test_display.py +++ b/tests/test_display.py @@ -138,7 +138,8 @@ def test_char_heights_match_size(): def test_get_chars_for_terminal_selects_largest_that_fits(monkeypatch): """Test that get_chars_for_terminal selects the largest size that fits both dimensions.""" - # Size requirements: 16(93w), 7(57w), 5(33w), 3(20w), 1(10w) + # Size requirements for displaying 00:00: + # 16(93w), 7(57w), 5(33w), 3(20w), 1(10w) # 80x24 terminal - size 7 fits (57w <= 80, 7h <= 24) monkeypatch.setattr("shutil.get_terminal_size", fake_size(80, 24)) @@ -219,3 +220,15 @@ def test_width_constraints_force_smaller_size(monkeypatch): chars = display.get_chars_for_terminal() height = len(chars["0"].splitlines()) assert height == 1, "19x5 terminal too narrow for size 3, should select size 1" + + +def test_three_digit_minutes_force_smaller_chars(monkeypatch): + """Wide minute values should fall back to smaller glyphs if needed.""" + # 60x20 terminal can show size 7 for two-digit minutes + monkeypatch.setattr("shutil.get_terminal_size", fake_size(60, 20)) + chars = display.get_chars_for_terminal(0) + assert len(chars["0"].splitlines()) == 7 + + # But 100 minutes needs 70 columns at size 7, so we should drop to size 5 + chars = display.get_chars_for_terminal(6000) # 100 minutes + assert len(chars["0"].splitlines()) == 5 diff --git a/tests/test_timer.py b/tests/test_timer.py index bce6a41..bcbf571 100644 --- a/tests/test_timer.py +++ b/tests/test_timer.py @@ -34,6 +34,10 @@ def test_duration_10_minutes(): assert timer.duration("10m") == 600 +def test_duration_150_minutes(): + assert timer.duration("150m") == 9000 + + def test_duration_25_minutes(): assert timer.duration("25m") == 1500 @@ -88,6 +92,18 @@ def test_get_number_lines_45_minutes(): ).strip("\n") +def test_get_number_lines_101_minutes(): + # Use size 5 digits + chars = CHARS_BY_SIZE[5] + assert join_lines(timer.get_number_lines(6060, chars)) == ( + " ██ ██████ ██ ██████ ██████\n" + " ███ ██ ██ ███ ██ ██ ██ ██ ██\n" + " ██ ██ ██ ██ ██ ██ ██ ██\n" + " ██ ██ ██ ██ ██ ██ ██ ██ ██\n" + " ██ ██████ ██ ██████ ██████" + ) + + def test_get_number_lines_17_minutes_and_four_seconds(): # Use size 5 digits chars = CHARS_BY_SIZE[5]