Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/countdown/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
33 changes: 23 additions & 10 deletions src/countdown/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/countdown/timer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)?
$
Expand Down
15 changes: 14 additions & 1 deletion tests/test_display.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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
16 changes: 16 additions & 0 deletions tests/test_timer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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]
Expand Down
Loading